diff --git a/erpnext/__init__.py b/erpnext/__init__.py index bb94383aed..d50b5bf560 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '10.1.70' +__version__ = '10.1.71' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 3da54f0611..263b5bb75e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -514,25 +514,9 @@ frappe.ui.form.on("Purchase Invoice", { me.frm.set_df_property("apply_tds", "read_only", 1); } - $.each(["warehouse", "rejected_warehouse"], function(i, field) { - frm.set_query(field, "items", function() { - return { - filters: [ - ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], - ["Warehouse", "is_group", "=", 0] - ] - } - }) - }) - - frm.set_query("supplier_warehouse", function() { - return { - filters: [ - ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], - ["Warehouse", "is_group", "=", 0] - ] - } - }) + erpnext.queries.setup_queries(frm, "Warehouse", function() { + return erpnext.queries.warehouse(frm.doc); + }); }, is_subcontracted: function(frm) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 504d45f64f..bd55c28ddd 100755 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, @@ -1485,6 +1486,207 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sec_warehouse", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "update_stock", + "fieldname": "set_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Set Accepted Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "update_stock", + "description": "Warehouse where you are maintaining stock of rejected items", + "fieldname": "rejected_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Rejected Warehouse", + "length": 0, + "no_copy": 1, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "col_break_warehouse", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "No", + "fieldname": "is_subcontracted", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Raw Materials Supplied", + "length": 0, + "no_copy": 0, + "options": "No\nYes", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.is_subcontracted==\"Yes\"", + "fieldname": "supplier_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Supplier Warehouse", + "length": 0, + "no_copy": 1, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0, + "width": "50px" + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1551,6 +1753,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scan_barcode", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scan Barcode", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 1, "allow_in_quick_entry": 0, @@ -1585,6 +1819,73 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "supplied_items", + "columns": 0, + "depends_on": "", + "fieldname": "raw_materials_supplied", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Raw Materials Supplied", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "supplied_items", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Supplied Items", + "length": 0, + "no_copy": 0, + "options": "Purchase Receipt Item Supplied", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -3723,140 +4024,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "raw_materials_supplied", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Materials Supplied", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "No", - "fieldname": "is_subcontracted", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Materials Supplied", - "length": 0, - "no_copy": 0, - "options": "No\nYes", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplier_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplier Warehouse", - "length": 0, - "no_copy": 1, - "options": "Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, - "width": "50px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplied_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplied Items", - "length": 0, - "no_copy": 0, - "options": "Purchase Receipt Item Supplied", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -4351,40 +4518,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Warehouse where you are maintaining stock of rejected items", - "fieldname": "rejected_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Rejected Warehouse", - "length": 0, - "no_copy": 1, - "options": "Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -4593,7 +4726,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-09-11 14:44:31.220376", + "modified": "2018-11-13 19:55:58.018816", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index f2a5c16bad..7348e1f8ec 100755 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -165,9 +165,12 @@ def get_items_list(pos_profile, company): select i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no, i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image, - id.expense_account, id.selling_cost_center, id.default_warehouse + id.expense_account, id.selling_cost_center, id.default_warehouse, + i.sales_uom, c.conversion_factor from - `tabItem` i LEFT JOIN `tabItem Default` id ON id.parent = i.name and id.company = %s + `tabItem` i + left join `tabItem Default` id on id.parent = i.name and id.company = %s + left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom where i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 {cond} @@ -534,6 +537,7 @@ def validate_item(doc): item_doc.item_name = item.get('item_name') item_doc.description = item.get('description') item_doc.stock_uom = item.get('stock_uom') + item_doc.uom = item.get('uom') item_doc.item_group = item.get('item_group') item_doc.append('item_defaults', { "company": doc.get("company"), diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index b8ea205d48..a4588b3dd8 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -939,7 +939,9 @@ var set_primary_action= function(frm, dialog, $results, invoice_healthcare_servi dialog.set_primary_action(__('Add'), function() { let checked_values = get_checked_values($results); if(checked_values.length > 0){ - frm.set_value("patient", dialog.fields_dict.patient.input.value); + if(invoice_healthcare_services) { + frm.set_value("patient", dialog.fields_dict.patient.input.value); + } frm.set_value("items", []); add_to_item_line(frm, checked_values, invoice_healthcare_services); dialog.hide(); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 4154d2ed98..09e952bd48 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, @@ -1585,6 +1586,71 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sec_warehouse", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "update_stock", + "fieldname": "set_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Set Source Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1651,6 +1717,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scan_barcode", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scan Barcode", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 1, "allow_in_quick_entry": 0, @@ -5546,7 +5644,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-09-07 14:24:58.854289", + "modified": "2018-11-12 20:01:21.289303", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 314b91bea5..91f3711f8d 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1407,6 +1407,8 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ this.child.item_code = this.items[0].item_code; this.child.item_name = this.items[0].item_name; this.child.stock_uom = this.items[0].stock_uom; + this.child.uom = this.items[0].sales_uom || this.items[0].stock_uom; + this.child.conversion_factor = this.items[0].conversion_factor || 1; this.child.brand = this.items[0].brand; this.child.description = this.items[0].description || this.items[0].item_name; this.child.discount_percentage = 0.0; @@ -1416,8 +1418,8 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ this.child.income_account = this.pos_profile_data['income_account'] || this.items[0].income_account; this.child.warehouse = (this.item_serial_no[this.child.item_code] ? this.item_serial_no[this.child.item_code][1] : (this.pos_profile_data['warehouse'] || this.items[0].default_warehouse)); - this.child.price_list_rate = flt(this.price_list_data[this.child.item_code], 9) / flt(this.frm.doc.conversion_rate, 9); - this.child.rate = flt(this.price_list_data[this.child.item_code], 9) / flt(this.frm.doc.conversion_rate, 9); + this.child.price_list_rate = flt(this.price_list_data[this.child.item_code] * this.child.conversion_factor, 9) / flt(this.frm.doc.conversion_rate, 9); + this.child.rate = flt(this.price_list_data[this.child.item_code] * this.child.conversion_factor, 9) / flt(this.frm.doc.conversion_rate, 9); this.child.actual_qty = me.get_actual_qty(this.items[0]); this.child.amount = flt(this.child.qty) * flt(this.child.rate); this.child.batch_no = this.item_batch_no[this.child.item_code]; diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 602e671ba6..2826760dd8 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -196,8 +196,9 @@ frappe.query_reports["General Ledger"] = { "fieldname":"group_by", "label": __("Group by"), "fieldtype": "Select", - "options": ["", "Group by Voucher", "Group by Account", "Group by Party"], - "default": "Group by Voucher" + "options": ["", __("Group by Voucher"), __("Group by Voucher (Consolidated)"), + __("Group by Account"), __("Group by Party")], + "default": __("Group by Voucher (Consolidated)") }, { "fieldname":"tax_id", diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 56663d37c4..524f5f70d7 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -16,6 +16,8 @@ def execute(filters=None): return [], [] account_details = {} + if not filters.get("group_by"): + filters['group_by'] = _('Group by Voucher (Consolidated)') if filters and filters.get('print_in_account_currency') and \ not filters.get('account'): @@ -48,11 +50,12 @@ def validate_filters(filters, account_details): if filters.get("account") and not account_details.get(filters.account): frappe.throw(_("Account {0} does not exists").format(filters.account)) - if (filters.get("account") and filters.get("group_by") == 'Group by Account' + if (filters.get("account") and filters.get("group_by") == _('Group by Account') and account_details[filters.account].is_group == 0): frappe.throw(_("Can not filter based on Account, if grouped by Account")) - if (filters.get("voucher_no") and filters.get("group_by") == 'Group by Voucher'): + if (filters.get("voucher_no") + and filters.get("group_by") in [_('Group by Voucher'), _('Group by Voucher (Consolidated)')]): frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher")) if filters.from_date > filters.to_date: @@ -114,30 +117,37 @@ def get_result(filters, account_details): return result - def get_gl_entries(filters): currency_map = get_currency(filters) - select_fields = """, debit_in_account_currency, - credit_in_account_currency""" \ + select_fields = """, debit, credit, debit_in_account_currency, + credit_in_account_currency """ - order_by_fields = "posting_date, account" - if filters.get("group_by") == "Group by Voucher": - order_by_fields = "posting_date, voucher_type, voucher_no" + group_by_statement = '' + order_by_statement = "order by posting_date, account" + + if filters.get("group_by") == _("Group by Voucher"): + order_by_statement = "order by posting_date, voucher_type, voucher_no" + + if filters.get("group_by") == _("Group by Voucher (Consolidated)"): + group_by_statement = "group by voucher_type, voucher_no, account, cost_center" + select_fields = """, sum(debit) as debit, sum(credit) as credit, + sum(debit_in_account_currency) as debit_in_account_currency, + sum(credit_in_account_currency) as credit_in_account_currency""" gl_entries = frappe.db.sql( """ select posting_date, account, party_type, party, - debit, credit, voucher_type, voucher_no, cost_center, project, against_voucher_type, against_voucher, account_currency, remarks, against, is_opening {select_fields} from `tabGL Entry` - where company=%(company)s {conditions} - order by {order_by_fields} + where company=%(company)s {conditions} {group_by_statement} + {order_by_statement} """.format( select_fields=select_fields, conditions=get_conditions(filters), - order_by_fields=order_by_fields + group_by_statement=group_by_statement, + order_by_statement=order_by_statement ), filters, as_dict=1) @@ -204,13 +214,13 @@ def get_data_with_opening_closing(filters, account_details, gl_entries): # Opening for filtered account data.append(totals.opening) - if filters.get("group_by"): + if filters.get("group_by") != _('Group by Voucher (Consolidated)'): for acc, acc_dict in iteritems(gle_map): # acc if acc_dict.entries: # opening data.append({}) - if filters.get("group_by") != "Group by Voucher": + if filters.get("group_by") != _("Group by Voucher"): data.append(acc_dict.totals.opening) data += acc_dict.entries @@ -219,10 +229,9 @@ def get_data_with_opening_closing(filters, account_details, gl_entries): data.append(acc_dict.totals.total) # closing - if filters.get("group_by") != "Group by Voucher": + if filters.get("group_by") != _("Group by Voucher"): data.append(acc_dict.totals.closing) data.append({}) - else: data += entries @@ -234,7 +243,6 @@ def get_data_with_opening_closing(filters, account_details, gl_entries): return data - def get_totals_dict(): def _get_debit_credit_dict(label): return _dict( @@ -251,12 +259,12 @@ def get_totals_dict(): ) def group_by_field(group_by): - if group_by == 'Group by Party': + if group_by == _('Group by Party'): return 'party' - elif group_by == 'Group by Voucher': - return 'voucher_no' - else: + elif group_by in [_('Group by Voucher (Consolidated)'), _('Group by Account')]: return 'account' + else: + return 'voucher_no' def initialize_gle_map(gl_entries, filters): gle_map = frappe._dict() @@ -291,7 +299,7 @@ def get_accountwise_gle(filters, gl_entries, gle_map): elif gle.posting_date <= to_date: update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle) update_value_in_dict(totals, 'total', gle) - if filters.get("group_by"): + if filters.get("group_by") != _('Group by Voucher (Consolidated)'): gle_map[gle.get(group_by)].entries.append(gle) else: entries.append(gle) @@ -301,7 +309,6 @@ def get_accountwise_gle(filters, gl_entries, gle_map): return totals, entries - def get_result_as_list(data, filters): balance, balance_in_account_currency = 0, 0 inv_details = get_supplier_invoice_details() diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js index 29909f8bb9..a6e6974c48 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -13,7 +13,11 @@ frappe.ui.form.on('Asset Value Adjustment', { } }); }, - + onload: function(frm) { + if(frm.is_new() && frm.doc.asset) { + frm.trigger("set_current_asset_value"); + } + }, asset: function(frm) { frm.trigger("set_current_asset_value"); }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 601af69092..c6f4fcc50c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, @@ -180,39 +181,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "No", - "fieldname": "is_subcontracted", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Supply Raw Materials", - "length": 0, - "no_copy": 0, - "options": "No\nYes", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -246,40 +214,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.is_subcontracted==\"Yes\"", - "fieldname": "supplier_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplier Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1353,6 +1287,168 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sec_warehouse", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "set_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Set Target Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "col_break_warehouse", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "No", + "fieldname": "is_subcontracted", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 1, + "label": "Supply Raw Materials", + "length": 0, + "no_copy": 0, + "options": "No\nYes", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.is_subcontracted==\"Yes\"", + "fieldname": "supplier_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Supplier Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1386,6 +1482,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scan_barcode", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scan Barcode", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 1, "allow_in_quick_entry": 0, @@ -1420,6 +1548,74 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "supplied_items", + "columns": 0, + "fieldname": "raw_material_details", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Raw Materials Supplied", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", + "fieldname": "supplied_items", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Supplied Items", + "length": 0, + "no_copy": 0, + "oldfieldname": "po_raw_material_details", + "oldfieldtype": "Table", + "options": "Purchase Order Item Supplied", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -3428,107 +3624,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "supplied_items", - "columns": 0, - "fieldname": "raw_material_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Materials Supplied", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.is_subcontracted", - "fieldname": "supplied_items_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplied Items", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "supplied_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplied Items", - "length": 0, - "no_copy": 0, - "oldfieldname": "po_raw_material_details", - "oldfieldtype": "Table", - "options": "Purchase Order Item Supplied", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -3736,8 +3831,8 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-29 12:16:12.886021", - "modified_by": "nabinhait@gmail.com", + "modified": "2018-11-12 19:59:49.211145", + "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", "owner": "Administrator", diff --git a/erpnext/buying/report/purchase_analytics/__init__.py b/erpnext/buying/report/purchase_analytics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js new file mode 100644 index 0000000000..297ec51cb1 --- /dev/null +++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js @@ -0,0 +1,128 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Purchase Analytics"] = { + "filters": [ + { + fieldname: "tree_type", + label: __("Tree Type"), + fieldtype: "Select", + options: ["Supplier Group","Supplier","Item Group","Item"], + default: "Supplier", + reqd: 1 + }, + { + fieldname: "doc_type", + label: __("based_on"), + fieldtype: "Select", + options: ["Purchase Order","Purchase Receipt","Purchase Invoice"], + default: "Purchase Invoice", + reqd: 1 + }, + { + fieldname: "value_quantity", + label: __("Value Or Qty"), + fieldtype: "Select", + options: [ + { "value": "Value", "label": __("Value") }, + { "value": "Quantity", "label": __("Quantity") }, + ], + default: "Value", + reqd: 1 + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_start_date"), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_end_date"), + reqd: 1 + }, + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "range", + label: __("Range"), + fieldtype: "Select", + options: [ + { "value": "Weekly", "label": __("Weekly") }, + { "value": "Monthly", "label": __("Monthly") }, + { "value": "Quarterly", "label": __("Quarterly") }, + { "value": "Yearly", "label": __("Yearly") } + ], + default: "Monthly", + reqd: 1 + } + + ], + "formatter": function(value, row, column, data) { + if(!value){ + value = 0 + } + return value; + }, + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + events: { + onCheckRow: function(data) { + row_name = data[2].content; + row_values = data.slice(5).map(function (column) { + return column.content; + }) + + entry = { + 'name':row_name, + 'values':row_values + } + + let raw_data = frappe.query_report.chart.data; + let new_datasets = raw_data.datasets; + + var found = false; + + for(var i=0; i < new_datasets.length;i++){ + if(new_datasets[i].name == row_name){ + found = true; + new_datasets.splice(i,1); + break; + } + } + + if(!found){ + new_datasets.push(entry); + } + + let new_data = { + labels: raw_data.labels, + datasets: new_datasets + } + + setTimeout(() => { + frappe.query_report.chart.update(new_data) + },200) + + + setTimeout(() => { + frappe.query_report.chart.draw(true); + }, 800) + + frappe.query_report.raw_chart_data = new_data; + }, + } + }) + }, +} diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.json b/erpnext/buying/report/purchase_analytics/purchase_analytics.json new file mode 100644 index 0000000000..996e3eef45 --- /dev/null +++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "creation": "2018-10-05 16:08:24.156448", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2018-10-05 16:08:33.272201", + "modified_by": "Administrator", + "module": "Buying", + "name": "Purchase Analytics", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Order", + "report_name": "Purchase Analytics", + "report_type": "Script Report", + "roles": [ + { + "role": "Purchase Manager" + }, + { + "role": "Purchase User" + } + ] +} \ No newline at end of file diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.py b/erpnext/buying/report/purchase_analytics/purchase_analytics.py new file mode 100644 index 0000000000..0f949477b1 --- /dev/null +++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.py @@ -0,0 +1,8 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from erpnext.selling.report.sales_analytics.sales_analytics import Analytics + +def execute(filters=None): + return Analytics(filters).run() diff --git a/erpnext/config/buying.py b/erpnext/config/buying.py index 270519e4e8..e99b1d88aa 100644 --- a/erpnext/config/buying.py +++ b/erpnext/config/buying.py @@ -122,10 +122,10 @@ def get_data(): "icon": "fa fa-table", "items": [ { - "type": "page", - "name": "purchase-analytics", - "label": _("Purchase Analytics"), - "icon": "fa fa-bar-chart", + "type": "report", + "is_query_report": True, + "name": "Purchase Analytics", + "doctype": "Purchase Order" }, { "type": "report", diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index 16ca9145b4..621c2dc4e2 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -112,11 +112,12 @@ def get_data(): "is_query_report": True, "name": "Completed Work Orders", "doctype": "Work Order" - },{ - "type": "page", - "name": "production-analytics", - "label": _("Production Analytics"), - "icon": "fa fa-bar-chart", + }, + { + "type": "report", + "is_query_report": True, + "name": "Production Analytics", + "doctype": "Work Order" }, { "type": "report", diff --git a/erpnext/config/selling.py b/erpnext/config/selling.py index 029fdac284..94f3102831 100644 --- a/erpnext/config/selling.py +++ b/erpnext/config/selling.py @@ -185,10 +185,10 @@ def get_data(): "icon": "fa fa-table", "items": [ { - "type": "page", - "name": "sales-analytics", - "label": _("Sales Analytics"), - "icon": "fa fa-bar-chart", + "type": "report", + "is_query_report": True, + "name": "Sales Analytics", + "doctype": "Sales Order" }, { "type": "page", diff --git a/erpnext/config/setup.py b/erpnext/config/setup.py index e38b30d5e0..1a119e80e0 100644 --- a/erpnext/config/setup.py +++ b/erpnext/config/setup.py @@ -64,7 +64,7 @@ def get_data(): { "type": "help", "label": _("Users and Permissions"), - "youtube_id": "fnBoRhBrwR4" + "youtube_id": "8Slw1hsTmUI" }, { "type": "help", diff --git a/erpnext/config/stock.py b/erpnext/config/stock.py index abdca0d7d7..60eee71dfa 100644 --- a/erpnext/config/stock.py +++ b/erpnext/config/stock.py @@ -218,10 +218,10 @@ def get_data(): "doctype": "Item Price", }, { - "type": "page", - "name": "stock-analytics", - "label": _("Stock Analytics"), - "icon": "fa fa-bar-chart" + "type": "report", + "is_query_report": True, + "name": "Stock Analytics", + "doctype": "Stock Entry" }, { "type": "report", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 87b7942070..f48b48b6f8 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -12,7 +12,7 @@ app_license = "GNU General Public License (v3)" source_link = "https://github.com/frappe/erpnext" develop_version = '12.x.x-develop' -staging_version = '11.0.3-beta.20' +staging_version = '11.0.3-beta.21' error_report_email = "support@erpnext.com" diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index e74a375557..dbbf3d33f7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -104,13 +104,26 @@ frappe.ui.form.on('Production Plan', { } }); }, - + get_items_for_mr: function(frm) { frappe.call({ - method: "get_items_for_material_requests", + method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", freeze: true, - doc: frm.doc, - callback: function() { + args: {doc: frm.doc}, + callback: function(r) { + if(r.message) { + frm.set_value('mr_items', []); + $.each(r.message, function(i, d) { + var item = frm.add_child('mr_items'); + item.actual_qty = d.actual_qty; + item.item_code = d.item_code; + item.item_name = d.item_name; + item.min_order_qty = d.min_order_qty; + item.quantity = d.quantity; + item.sales_order = d.sales_order; + item.warehouse = d.warehouse; + }); + } refresh_field('mr_items'); } }); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 12f2f04e38..7d11ae4993 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -10,6 +10,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from six import string_types +from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults class ProductionPlan(Document): def validate(self): @@ -209,9 +210,10 @@ class ProductionPlan(Document): def set_status(self): self.status = { - '0': 'Draft', - '1': 'Submitted' - }[cstr(self.docstatus or 0)] + 0: 'Draft', + 1: 'Submitted', + 2: 'Cancelled' + }.get(self.docstatus) if self.total_produced_qty > 0: self.status = "In Process" @@ -281,102 +283,6 @@ class ProductionPlan(Document): return item_dict - def get_items_for_material_requests(self): - self.mr_items = [] - - for data in self.po_items: - bom_wise_item_details = {} - if not data.planned_qty: - frappe.throw(_("For row {0}: Enter planned qty").format(data.idx)) - - if data.include_exploded_items and data.bom_no and self.include_subcontracted_items: - for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom, - ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0) 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 - from - `tabBOM Explosion Item` bei - JOIN `tabBOM` bom ON bom.name = bei.parent - JOIN `tabItem` item ON item.name = bei.item_code - LEFT JOIN `tabItem Default` item_default - ON item_default.parent = item.name and item_default.company=%s - where - bei.docstatus < 2 - and bom.name=%s and item.is_stock_item in (1, {0}) - group by bei.item_code, bei.stock_uom""".format(0 if self.include_non_stock_items else 1), - (self.company, data.bom_no), as_dict=1): - bom_wise_item_details.setdefault(d.item_code, d) - else: - bom_wise_item_details = self.get_subitems(data, bom_wise_item_details, data.bom_no, 1) - - for item, item_details in bom_wise_item_details.items(): - if item_details.qty > 0: - self.add_item_in_material_request_items(item, item_details, data) - - def get_subitems(self, data, bom_wise_item_details, bom_no, parent_qty): - items = frappe.db.sql(""" - SELECT - bom_item.item_code, default_material_request_type, item.item_name, - ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, - item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, - item.default_bom as default_bom, bom_item.description as description, - bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, - item_default.default_warehouse - FROM - `tabBOM Item` bom_item - JOIN `tabBOM` bom ON bom.name = bom_item.parent - JOIN tabItem item ON bom_item.item_code = item.name - LEFT JOIN `tabItem Default` item_default - ON item.name = item_default.parent and item_default.company = %(company)s - where - bom.name = %(bom)s - and bom_item.docstatus < 2 - and item.is_stock_item in (1, {0}) - group by bom_item.item_code""".format(0 if self.include_non_stock_items else 1),{ - 'bom': bom_no, - 'parent_qty': parent_qty, - 'company': self.company - }, as_dict=1) - - for d in items: - if not data.include_exploded_items or not d.default_bom: - if d.item_code in bom_wise_item_details: - bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty - else: - bom_wise_item_details[d.item_code] = d - - if data.include_exploded_items and d.default_bom: - if ((d.default_material_request_type in ["Manufacture", "Purchase"] and - not d.is_sub_contracted) or (d.is_sub_contracted and self.include_subcontracted_items)): - if d.qty > 0: - self.get_subitems(data, bom_wise_item_details, d.default_bom, d.qty) - - return bom_wise_item_details - - def add_item_in_material_request_items(self, item, row, data): - total_qty = row.qty * data.planned_qty - projected_qty, actual_qty = get_bin_details(row) - - requested_qty = 0 - if self.ignore_existing_ordered_qty: - requested_qty = total_qty - else: - requested_qty = total_qty - projected_qty - - if requested_qty > 0 and requested_qty < row.min_order_qty: - requested_qty = row.min_order_qty - - if requested_qty > 0: - self.append('mr_items', { - 'item_code': item, - 'item_name': row.item_name, - 'quantity': requested_qty, - 'warehouse': row.source_warehouse or row.default_warehouse, - 'actual_qty': actual_qty, - 'min_order_qty': row.min_order_qty, - 'sales_order': data.sales_order - }) - def make_work_order(self): wo_list = [] self.validate_data() @@ -466,6 +372,87 @@ class ProductionPlan(Document): else : msgprint(_("No material request created")) +def get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items): + for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom, + ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0) 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 + from + `tabBOM Explosion Item` bei + JOIN `tabBOM` bom ON bom.name = bei.parent + JOIN `tabItem` item ON item.name = bei.item_code + LEFT JOIN `tabItem Default` item_default + ON item_default.parent = item.name and item_default.company=%s + where + bei.docstatus < 2 + and bom.name=%s and item.is_stock_item in (1, {0}) + group by bei.item_code, bei.stock_uom""".format(0 if include_non_stock_items else 1), + (company, bom_no), as_dict=1): + bom_wise_item_details.setdefault(d.get('item_code'), d) + return bom_wise_item_details + +def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, parent_qty): + items = frappe.db.sql(""" + SELECT + bom_item.item_code, default_material_request_type, item.item_name, + ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, + item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, + item.default_bom as default_bom, bom_item.description as description, + bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, + item_default.default_warehouse + FROM + `tabBOM Item` bom_item + JOIN `tabBOM` bom ON bom.name = bom_item.parent + JOIN tabItem item ON bom_item.item_code = item.name + LEFT JOIN `tabItem Default` item_default + ON item.name = item_default.parent and item_default.company = %(company)s + where + bom.name = %(bom)s + and bom_item.docstatus < 2 + and item.is_stock_item in (1, {0}) + group by bom_item.item_code""".format(0 if include_non_stock_items else 1),{ + 'bom': bom_no, + 'parent_qty': parent_qty, + 'company': company + }, as_dict=1) + + for d in items: + if not data.get('include_exploded_items') or not d.default_bom: + if d.item_code in bom_wise_item_details: + bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty + else: + bom_wise_item_details[d.item_code] = d + + if data.get('include_exploded_items') and d.default_bom: + if ((d.default_material_request_type in ["Manufacture", "Purchase"] and + not d.is_sub_contracted) or (d.is_sub_contracted and include_subcontracted_items)): + if d.qty > 0: + get_subitems(doc, data, bom_wise_item_details, d.default_bom, company, include_non_stock_items, include_subcontracted_items, d.qty) + return bom_wise_item_details + +def add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, row, data, warehouse, company): + total_qty = row.qty * planned_qty + projected_qty, actual_qty = get_bin_details(row) + + requested_qty = 0 + if ignore_existing_ordered_qty: + requested_qty = total_qty + else: + requested_qty = total_qty - projected_qty + if requested_qty > 0 and requested_qty < row.min_order_qty: + requested_qty = row.min_order_qty + item_group_defaults = get_item_group_defaults(item, company) + if requested_qty > 0: + doc.setdefault('mr_items', []).append({ + 'item_code': item, + 'item_name': row.item_name, + 'quantity': requested_qty, + 'warehouse': warehouse or row.source_warehouse or row.default_warehouse or item_group_defaults.get("default_warehouse"), + 'actual_qty': actual_qty, + 'min_order_qty': row.min_order_qty, + 'sales_order': data.get('sales_order') + }) + def get_sales_orders(self): so_filter = item_filter = "" if self.from_date: @@ -520,3 +507,46 @@ def get_bin_details(row): """.format(conditions=conditions), { "item_code": row.item_code }, as_list=1) return item_projected_qty and item_projected_qty[0] or (0,0) + +@frappe.whitelist() +def get_items_for_material_requests(doc, company=None): + if isinstance(doc, string_types): + doc = frappe._dict(json.loads(doc)) + + doc['mr_items'] = [] + po_items = doc['po_items'] if doc.get('po_items') else doc['items'] + + for data in po_items: + warehouse = None + bom_wise_item_details = {} + + if data.get('required_qty'): + planned_qty = data.get('required_qty') + bom_no = data.get('bom') + ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') + include_non_stock_items = 1 + warehouse = data.get('for_warehouse') + if data.get('include_exploded_items'): + include_subcontracted_items = 1 + else: + include_subcontracted_items = 0 + else: + planned_qty = data.get('planned_qty') + bom_no = data.get('bom_no') + include_subcontracted_items = doc['include_subcontracted_items'] + company = doc['company'] + include_non_stock_items = doc['include_non_stock_items'] + ignore_existing_ordered_qty = doc['ignore_existing_ordered_qty'] + if not planned_qty: + frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx'))) + + if data.get('include_exploded_items') and bom_no and include_subcontracted_items: + # fetch exploded items from BOM + bom_wise_item_details = get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items) + else: + bom_wise_item_details = get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, 1) + for item, item_details in bom_wise_item_details.items(): + if item_details.qty > 0: + add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, item_details, data, warehouse, company) + + return doc['mr_items'] diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 7cf426858d..a33d42b7d0 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -10,6 +10,7 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests class TestProductionPlan(unittest.TestCase): def setUp(self): @@ -160,7 +161,9 @@ def create_production_plan(**args): 'planned_start_date': args.planned_start_date or now_datetime() }] }) - pln.get_items_for_material_requests() + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + pln.append('mr_items', d) if not args.do_not_save: pln.insert() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 1d465d57ae..e73328f10e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -448,7 +448,9 @@ class WorkOrder(Document): if item_dict.get(d.item_code): d.required_qty = item_dict.get(d.item_code).get("qty") else: - for item in sorted(item_dict.values(), key=lambda d: d['idx']): + # Attribute a big number (999) to idx for sorting putpose in case idx is NULL + # For instance in BOM Explosion Item child table, the items coming from sub assembly items + for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999): self.append('required_items', { 'operation': item.operation, 'item_code': item.item_code, diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js index 049a822ec0..2ac6fa073b 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js @@ -16,6 +16,22 @@ frappe.query_reports["BOM Stock Report"] = { "fieldname": "show_exploded_view", "label": __("Show exploded view"), "fieldtype": "Check" + }, { + "fieldname": "qty_to_produce", + "label": __("Quantity to Produce"), + "fieldtype": "Int", + "default": "1" + }, + ], + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (column.id == "Item"){ + if (data["Enough Parts to Build"] > 0){ + value = `${data['Item']}` + } else { + value = `${data['Item']}` + } } - ] + return value + } } diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 32368395c0..ec3672025b 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -7,6 +7,7 @@ from frappe import _ def execute(filters=None): if not filters: filters = {} + columns = get_columns() data = get_bom_stock(filters) @@ -18,6 +19,7 @@ def get_columns(): columns = [ _("Item") + ":Link/Item:150", _("Description") + "::500", + _("Qty per BOM Line") + ":Float:100", _("Required Qty") + ":Float:100", _("In Stock Qty") + ":Float:100", _("Enough Parts to Build") + ":Float:200", @@ -32,6 +34,10 @@ def get_bom_stock(filters): table = "`tabBOM Item`" qty_field = "qty" + qty_to_produce = filters.get("qty_to_produce", 1) + if int(qty_to_produce) <= 0: + frappe.throw(_("Quantity to Produce can not be less than Zero")) + if filters.get("show_exploded_view"): table = "`tabBOM Explosion Item`" qty_field = "stock_qty" @@ -50,11 +56,12 @@ def get_bom_stock(filters): return frappe.db.sql(""" SELECT - bom_item.item_code , + bom_item.item_code, bom_item.description , bom_item.{qty_field}, + bom_item.{qty_field} * {qty_to_produce}, sum(ledger.actual_qty) as actual_qty, - sum(FLOOR(ledger.actual_qty / bom_item.{qty_field}))as to_build + sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce}))) FROM {table} AS bom_item LEFT JOIN `tabBin` AS ledger @@ -63,4 +70,10 @@ def get_bom_stock(filters): WHERE bom_item.parent = '{bom}' and bom_item.parenttype='BOM' - GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom)) + GROUP BY bom_item.item_code""".format( + qty_field=qty_field, + table=table, + conditions=conditions, + bom=bom, + qty_to_produce=qty_to_produce or 1) + ) diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index 0602193d51..7447a1f670 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -3,17 +3,16 @@ from __future__ import unicode_literals import frappe -from frappe import _ +from frappe import _, scrub from frappe.utils import getdate -from erpnext.selling.report.sales_analytics.sales_analytics import (get_period_date_ranges,get_period) +from erpnext.stock.report.stock_analytics.stock_analytics import (get_period_date_ranges, get_period) def execute(filters=None): columns = get_columns(filters) - data, chart = get_data(filters,columns) - return columns, data,None ,chart + data, chart = get_data(filters, columns) + return columns, data, None , chart def get_columns(filters): - columns =[ { "label": _("Status"), @@ -22,122 +21,113 @@ def get_columns(filters): "width": 140 }] - ranges = get_period_date_ranges(period=filters["range"], year_start_date = filters["from_date"],year_end_date=filters["to_date"]) + ranges = get_period_date_ranges(filters) for dummy, end_date in ranges: - label = field_name = get_period(end_date,filters["range"]) + period = get_period(end_date, filters) - columns.append( - { - "label": _(label), - "fieldname": field_name, + columns.append({ + "label": _(period), + "fieldname": scrub(period), "fieldtype": "Float", "width": 120 - }, - ) + }) return columns -def get_data_list(filters,entry): - - data_list = { - "All Work Orders" : {}, - "Not Started" : {}, - "Overdue" : {}, - "Pending" : {}, - "Completed" : {} +def get_periodic_data(filters, entry): + periodic_data = { + "All Work Orders": {}, + "Not Started": {}, + "Overdue": {}, + "Pending": {}, + "Completed": {} } - ranges = get_period_date_ranges(period=filters["range"], year_start_date = filters["from_date"],year_end_date=filters["to_date"]) + ranges = get_period_date_ranges(filters) - for from_date,end_date in ranges: - period = get_period(end_date,filters["range"]) + for from_date, end_date in ranges: + period = get_period(end_date, filters) for d in entry: if getdate(d.creation) <= getdate(from_date) or getdate(d.creation) <= getdate(end_date) : - data_list = update_data_list(data_list,"All Work Orders",period) - + periodic_data = update_periodic_data(periodic_data, "All Work Orders", period) if d.status == 'Completed': if getdate(d.actual_end_date) < getdate(from_date) or getdate(d.modified) < getdate(from_date): - data_list = update_data_list(data_list, "Completed",period) + periodic_data = update_periodic_data(periodic_data, "Completed", period) elif getdate(d.actual_start_date) < getdate(from_date) : - data_list = update_data_list(data_list, "Pending", period) + periodic_data = update_periodic_data(periodic_data, "Pending", period) elif getdate(d.planned_start_date) < getdate(from_date) : - data_list = update_data_list(data_list, "Overdue", period) + periodic_data = update_periodic_data(periodic_data, "Overdue", period) else: - data_list = update_data_list(data_list, "Not Started", period) + periodic_data = update_periodic_data(periodic_data, "Not Started", period) elif d.status == 'In Process': if getdate(d.actual_start_date) < getdate(from_date) : - data_list = update_data_list(data_list, "Pending", period) + periodic_data = update_periodic_data(periodic_data, "Pending", period) elif getdate(d.planned_start_date) < getdate(from_date) : - data_list = update_data_list(data_list, "Overdue", period) + periodic_data = update_periodic_data(periodic_data, "Overdue", period) else: - data_list = update_data_list(data_list, "Not Started", period) + periodic_data = update_periodic_data(periodic_data, "Not Started", period) elif d.status == 'Not Started': if getdate(d.planned_start_date) < getdate(from_date) : - data_list = update_data_list(data_list, "Overdue", period) + periodic_data = update_periodic_data(periodic_data, "Overdue", period) else: - data_list = update_data_list(data_list, "Not Started", period) - return data_list + periodic_data = update_periodic_data(periodic_data, "Not Started", period) + return periodic_data -def update_data_list(data_list, status, period): - if data_list.get(status).get(period): - data_list[status][period] += 1 +def update_periodic_data(periodic_data, status, period): + if periodic_data.get(status).get(period): + periodic_data[status][period] += 1 else: - data_list[status][period] = 1 + periodic_data[status][period] = 1 - return data_list - -def get_data(filters,columns): + return periodic_data +def get_data(filters, columns): data = [] - entry = frappe.get_all("Work Order", fields=["creation", "modified", "actual_start_date", "actual_end_date", "planned_start_date", "planned_end_date", "status"], - filters={"docstatus" : 1, "company" : filters["company"] }) + filters={"docstatus": 1, "company": filters["company"] }) - data_list = get_data_list(filters,entry) + periodic_data = get_periodic_data(filters,entry) labels = ["All Work Orders", "Not Started", "Overdue", "Pending", "Completed"] - - chart_data = get_chart_data(data_list,columns) - - ranges = get_period_date_ranges(period=filters["range"], year_start_date = filters["from_date"],year_end_date=filters["to_date"]) + chart_data = get_chart_data(periodic_data,columns) + ranges = get_period_date_ranges(filters) for label in labels: work = {} work["Status"] = label for dummy,end_date in ranges: - period = get_period(end_date,filters["range"]) - if data_list.get(label).get(period): - work[period] = data_list.get(label).get(period) + period = get_period(end_date, filters) + if periodic_data.get(label).get(period): + work[scrub(period)] = periodic_data.get(label).get(period) else: - work[period] = 0.0 + work[scrub(period)] = 0.0 data.append(work) return data, chart_data -def get_chart_data(data_list,columns): - +def get_chart_data(periodic_data, columns): labels = [d.get("label") for d in columns[1:]] all_data, not_start, overdue, pending, completed = [], [], [] , [], [] datasets = [] for d in labels: - all_data.append(data_list.get("All Work Orders").get(d)) - not_start.append(data_list.get("Not Started").get(d)) - overdue.append(data_list.get("Overdue").get(d)) - pending.append(data_list.get("Pending").get(d)) - completed.append(data_list.get("Completed").get(d)) + all_data.append(periodic_data.get("All Work Orders").get(d)) + not_start.append(periodic_data.get("Not Started").get(d)) + overdue.append(periodic_data.get("Overdue").get(d)) + pending.append(periodic_data.get("Pending").get(d)) + completed.append(periodic_data.get("Completed").get(d)) datasets.append({'name':'All Work Orders', 'values': all_data}) datasets.append({'name':'Not Started', 'values': not_start}) @@ -148,10 +138,9 @@ def get_chart_data(data_list,columns): chart = { "data": { 'labels': labels, - 'datasets':datasets + 'datasets': datasets } } - chart["type"] = "line" return chart diff --git a/erpnext/patches.txt b/erpnext/patches.txt old mode 100644 new mode 100755 index 9c569488c9..ca256679e8 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -568,7 +568,13 @@ erpnext.patches.v11_0.remove_land_unit_icon erpnext.patches.v11_0.add_default_dispatch_notification_template erpnext.patches.v11_0.add_market_segments erpnext.patches.v11_0.add_sales_stages +execute:frappe.delete_doc("Page", "Sales Analytics") +execute:frappe.delete_doc("Page", "Purchase Analytics") +execute:frappe.delete_doc("Page", "Stock Analytics") +execute:frappe.delete_doc("Page", "Production Analytics") erpnext.patches.v11_0.ewaybill_fields_gst_india erpnext.patches.v11_0.drop_column_max_days_allowed erpnext.patches.v11_0.change_healthcare_desktop_icons erpnext.patches.v10_0.update_user_image_in_employee +erpnext.patches.v11_0.update_delivery_trip_status +erpnext.patches.v10_0.repost_gle_for_purchase_receipts_with_rejected_items diff --git a/erpnext/patches/v10_0/repost_gle_for_purchase_receipts_with_rejected_items.py b/erpnext/patches/v10_0/repost_gle_for_purchase_receipts_with_rejected_items.py new file mode 100644 index 0000000000..68c06ef62b --- /dev/null +++ b/erpnext/patches/v10_0/repost_gle_for_purchase_receipts_with_rejected_items.py @@ -0,0 +1,32 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe, erpnext + +def execute(): + for company in frappe.get_all("Company"): + if not erpnext.is_perpetual_inventory_enabled(company.name): + continue + + acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto") or "1900-01-01" + pr_with_rejected_warehouse = frappe.db.sql(""" + select pr.name + from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item + where pr.name = pr_item.parent + and pr.posting_date > %s + and pr.docstatus=1 + and pr.company = %s + and pr_item.rejected_qty > 0 + """, (acc_frozen_upto, company.name), as_dict=1) + + for d in pr_with_rejected_warehouse: + doc = frappe.get_doc("Purchase Receipt", d.name) + + doc.docstatus = 2 + doc.make_gl_entries_on_cancel(repost_future_gle=False) + + + # update gl entries for submit state of PR + doc.docstatus = 1 + doc.make_gl_entries(repost_future_gle=False) diff --git a/erpnext/patches/v11_0/update_delivery_trip_status.py b/erpnext/patches/v11_0/update_delivery_trip_status.py new file mode 100755 index 0000000000..64b3063bac --- /dev/null +++ b/erpnext/patches/v11_0/update_delivery_trip_status.py @@ -0,0 +1,27 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc('stock', 'doctype', 'delivery_trip') + frappe.reload_doc('stock', 'doctype', 'delivery_stop', force=True) + + for trip in frappe.get_all("Delivery Trip"): + trip_doc = frappe.get_doc("Delivery Trip", trip.name) + + status = { + 0: "Draft", + 1: "Scheduled", + 2: "Cancelled" + }[trip_doc.docstatus] + + if trip_doc.docstatus == 1: + visited_stops = [stop.visited for stop in trip_doc.delivery_stops] + if all(visited_stops): + status = "Completed" + elif any(visited_stops): + status = "In Transit" + + frappe.db.set_value("Delivery Trip", trip.name, "status", status, update_modified=False) diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 9f1c58601d..8c84c11ae9 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -128,6 +128,50 @@ class TestTimesheet(unittest.TestCase): settings.ignore_employee_time_overlap = initial_setting settings.save() + def test_timesheet_std_working_hours(self): + company = frappe.get_doc('Company', "_Test Company") + company.standard_working_hours = 8 + company.save() + + timesheet = frappe.new_doc("Timesheet") + timesheet.employee = "_T-Employee-00001" + timesheet.company = '_Test Company' + timesheet.append( + 'time_logs', + { + "activity_type": "_Test Activity Type", + "from_time": now_datetime(), + "to_time": now_datetime() + datetime.timedelta(days= 4) + } + ) + timesheet.save() + + ts = frappe.get_doc('Timesheet', timesheet.name) + self.assertEqual(ts.total_hours, 32) + ts.submit() + ts.cancel() + + company = frappe.get_doc('Company', "_Test Company") + company.standard_working_hours = 0 + company.save() + + timesheet = frappe.new_doc("Timesheet") + timesheet.employee = "_T-Employee-00001" + timesheet.company = '_Test Company' + timesheet.append( + 'time_logs', + { + "activity_type": "_Test Activity Type", + "from_time": now_datetime(), + "to_time": now_datetime() + datetime.timedelta(days= 4) + } + ) + timesheet.save() + + ts = frappe.get_doc('Timesheet', timesheet.name) + self.assertEqual(ts.total_hours, 96) + ts.submit() + ts.cancel() def make_salary_structure_for_timesheet(employee): salary_structure_name = "Timesheet Salary Structure Test" diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 5234df67ff..e890befd1a 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -90,6 +90,13 @@ frappe.ui.form.on("Timesheet", { } }, + company: function(frm) { + frappe.db.get_value('Company', { 'company_name' : frm.doc.company }, 'standard_working_hours') + .then(({ message }) => { + (frappe.working_hours = message.standard_working_hours || 0); + }); + }, + make_invoice: function(frm) { let dialog = new frappe.ui.Dialog({ title: __("Select Item (optional)"), @@ -142,11 +149,21 @@ frappe.ui.form.on("Timesheet Detail", { to_time: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; + var time_diff = (moment(child.to_time).diff(moment(child.from_time),"seconds")) / ( 60 * 60 * 24); + var std_working_hours = 0; if(frm._setting_hours) return; - frappe.model.set_value(cdt, cdn, "hours", moment(child.to_time).diff(moment(child.from_time), - "seconds") / 3600); + + var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600; + std_working_hours = time_diff * frappe.working_hours; + + if (std_working_hours < hours && std_working_hours > 0) { + frappe.model.set_value(cdt, cdn, "hours", std_working_hours); + } else { + frappe.model.set_value(cdt, cdn, "hours", hours); + } }, + time_logs_add: function(frm) { var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row'); $trigger_again.on('click', () => { @@ -209,17 +226,23 @@ var calculate_end_time = function(frm, cdt, cdn) { let d = moment(child.from_time); if(child.hours) { - d.add(child.hours, "hours"); - frm._setting_hours = true; - frappe.model.set_value(cdt, cdn, "to_time", - d.format(frappe.defaultDatetimeFormat)).then(() => { - frm._setting_hours = false; - }); - } + var time_diff = (moment(child.to_time).diff(moment(child.from_time),"seconds")) / (60 * 60 * 24); + var std_working_hours = 0; + var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600; + std_working_hours = time_diff * frappe.working_hours; - if((frm.doc.__islocal || frm.doc.__onload.maintain_bill_work_hours_same) && child.hours){ - frappe.model.set_value(cdt, cdn, "billing_hours", child.hours); + if (std_working_hours < hours && std_working_hours > 0) { + frappe.model.set_value(cdt, cdn, "hours", std_working_hours); + frappe.model.set_value(cdt, cdn, "to_time", d.add(hours, "hours").format(frappe.defaultDatetimeFormat)); + } else { + d.add(child.hours, "hours"); + frm._setting_hours = true; + frappe.model.set_value(cdt, cdn, "to_time", + d.format(frappe.defaultDatetimeFormat)).then(() => { + frm._setting_hours = false; + }); + } } } diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index f48c0c634b..4b466d2630 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -9,7 +9,7 @@ from frappe import _ import json from datetime import timedelta from erpnext.controllers.queries import get_match_cond -from frappe.utils import flt, time_diff_in_hours, get_datetime, getdate, cint +from frappe.utils import flt, time_diff_in_hours, get_datetime, getdate, cint, date_diff, add_to_date from frappe.model.document import Document from erpnext.manufacturing.doctype.workstation.workstation import (check_if_within_operating_hours, WorkstationHolidayError) @@ -27,6 +27,7 @@ class Timesheet(Document): self.set_status() self.validate_dates() self.validate_time_logs() + self.calculate_std_hours() self.update_cost() self.calculate_total_amounts() self.calculate_percentage_billed() @@ -93,6 +94,17 @@ class Timesheet(Document): self.start_date = getdate(start_date) self.end_date = getdate(end_date) + def calculate_std_hours(self): + std_working_hours = frappe.get_value("Company", self.company, 'standard_working_hours') + + for time in self.time_logs: + if time.from_time and time.to_time: + if flt(std_working_hours) > 0: + time.hours = flt(std_working_hours) * date_diff(time.to_time, time.from_time) + else: + if not time.hours: + time.hours = time_diff_in_hours(time.to_time, time.from_time) + def before_cancel(self): self.set_status() diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3e27d5638d..f3c29fce78 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -98,6 +98,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frm.cscript.calculate_taxes_and_totals(); }); + frappe.ui.form.on(this.frm.doctype + " Item", { + items_add: function(frm, cdt, cdn) { + var item = frappe.get_doc(cdt, cdn); + if(!item.warehouse && frm.doc.set_warehouse) { + item.warehouse = frm.doc.set_warehouse; + } + } + }); + var me = this; if(this.frm.fields_dict["items"].grid.get_field('batch_no')) { this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) { @@ -253,6 +262,62 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.set_dynamic_labels(); this.setup_sms(); this.setup_quality_inspection(); + this.frm.fields_dict["scan_barcode"] && this.frm.fields_dict["scan_barcode"].set_value(""); + this.frm.fields_dict["scan_barcode"] && this.frm.fields_dict["scan_barcode"].set_new_description(""); + }, + + scan_barcode: function() { + let scan_barcode_field = this.frm.fields_dict["scan_barcode"]; + + let show_description = function(idx, item_code, exist=null) { + if(exist) { + scan_barcode_field.set_new_description(__('Row : ') + idx + ' ' + + item_code + __(' Qty increased by 1')); + } else { + scan_barcode_field.set_new_description(__('New row : ') + idx + ' ' + + item_code + __(' Created')); + } + } + + 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", + args: { search_value: this.frm.doc.scan_barcode } + }).then(r => { + + if(r && r.message && r.message.item_code) { + let child = ""; + let add_row_index = -1; + let cur_grid= this.frm.fields_dict["items"].grid; + + this.frm.doc.items.map(d => { + if(d.item_code==r.message.item_code){ + add_row_index = d.idx; + return; + } else if(!d.item_code && add_row_index==-1) { + add_row_index = d.idx; + } + }); + + if(add_row_index == -1) { + child = frappe.model.add_child(this.frm.doc, cur_grid.doctype, "items", add_row_index); + } else { + child = cur_grid.get_grid_row(add_row_index-1).doc; + } + show_description(child.idx, r.message.item_code, child.item_code); + + frappe.model.set_value(child.doctype, child.name, { + "item_code": r.message.item_code, + "qty": (child.qty || 0) + 1 + }); + } + else{ + scan_barcode_field.set_new_description(this.frm.doc.scan_barcode +__(' does not exist!')); + } + }); + scan_barcode_field.set_value(""); + } + return false; }, apply_default_taxes: function() { @@ -1407,6 +1472,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }) } + }, + + set_warehouse: function() { + var me = this; + if(this.frm.doc.set_warehouse) { + $.each(this.frm.doc.items || [], function(i, item) { + frappe.model.set_value(me.frm.doctype + " Item", item.name, "warehouse", me.frm.doc.set_warehouse); + }); + } } }); diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 5275e2e296..54d7654c2d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -150,6 +150,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( && flt(doc.per_delivered, 6) < 100) { this.frm.add_custom_button(__('Material Request'), function() { me.make_material_request() }, __("Make")); + this.frm.add_custom_button(__('Request for Raw Materials'), + function() { me.make_raw_material_request() }, __("Make")); } // make purchase order @@ -313,6 +315,86 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( }) }, + make_raw_material_request: function() { + var me = this; + this.frm.call({ + doc: this.frm.doc, + method: 'get_work_order_items', + args: { + for_raw_material_request: 1 + }, + callback: function(r) { + if(!r.message) { + frappe.msgprint({ + message: __('No Items with Bill of Materials.'), + indicator: 'orange' + }); + return; + } + else { + me.make_raw_material_request_dialog(r); + } + } + }); + }, + + make_raw_material_request_dialog: function(r) { + var fields = [ + {fieldtype:'Check', fieldname:'include_exploded_items', + label: __('Include Exploded Items')}, + {fieldtype:'Check', fieldname:'ignore_existing_ordered_qty', + label: __('Ignore Existing Ordered Qty')}, + { + fieldtype:'Table', fieldname: 'items', + description: __('Select BOM, Qty and For Warehouse'), + fields: [ + {fieldtype:'Read Only', fieldname:'item_code', + label: __('Item Code'), in_list_view:1}, + {fieldtype:'Link', fieldname:'bom', options: 'BOM', reqd: 1, + label: __('BOM'), in_list_view:1, get_query: function(doc) { + return {filters: {item: doc.item_code}}; + } + }, + {fieldtype:'Float', fieldname:'required_qty', reqd: 1, + label: __('Qty'), in_list_view:1}, + {fieldtype:'Link', fieldname:'for_warehouse', options: 'Warehouse', + label: __('For Warehouse')} + ], + data: r.message, + get_data: function() { + return r.message + } + } + ] + var d = new frappe.ui.Dialog({ + title: __("Select from Items having BOM"), + fields: fields, + primary_action: function() { + var data = d.get_values(); + me.frm.call({ + method: 'erpnext.selling.doctype.sales_order.sales_order.make_raw_material_request', + args: { + items: data, + company: me.frm.doc.company, + sales_order: me.frm.docname, + project: me.frm.project + }, + freeze: true, + callback: function(r) { + if(r.message) { + frappe.msgprint(__('Material Request {0} submitted.', + ['' + r.message.name+ ''])); + } + d.hide(); + me.frm.reload_doc(); + } + }); + }, + primary_action_label: __('Make') + }); + d.show(); + }, + make_delivery_note_based_on_delivery_date: function() { var me = this; @@ -423,7 +505,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( filters: {'parent': me.frm.doc.name} } }}, - + {"fieldtype": "Button", "label": __("Make Purchase Order"), "fieldname": "make_purchase_order", "cssClass": "btn-primary"}, ] }); diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 22e574167c..0f3b6776ca 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, @@ -1171,6 +1172,70 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sec_warehouse", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "set_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Set Source Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1204,6 +1269,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scan_barcode", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scan Barcode", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 1, "allow_in_quick_entry": 0, @@ -3918,7 +4015,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 14:44:44.011356", + "modified": "2018-11-12 20:00:35.272747", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a7b4a3e2c9..5f435ced74 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -5,8 +5,9 @@ from __future__ import unicode_literals import frappe import json import frappe.utils -from frappe.utils import cstr, flt, getdate, comma_and, cint, nowdate +from frappe.utils import cstr, flt, getdate, comma_and, cint, nowdate, add_days from frappe import _ +from six import string_types from frappe.model.utils import get_fetch_values from frappe.model.mapper import get_mapped_doc from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty @@ -17,6 +18,7 @@ from frappe.desk.doctype.auto_repeat.auto_repeat import get_next_schedule_date from erpnext.selling.doctype.customer.customer import check_credit_limit 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.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -366,7 +368,7 @@ class SalesOrder(SellingController): self.indicator_color = "green" self.indicator_title = _("Paid") - def get_work_order_items(self): + def get_work_order_items(self, for_raw_material_request=0): '''Returns items with BOM that already do not have a linked work order''' items = [] @@ -375,8 +377,13 @@ class SalesOrder(SellingController): bom = get_default_bom_item(i.item_code) if bom: stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty - pending_qty= stock_qty - flt(frappe.db.sql('''select sum(qty) from `tabWork Order` + if not for_raw_material_request: + total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order` where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2''', (i.item_code, self.name, i.name))[0][0]) + pending_qty = stock_qty - total_work_order_qty + else: + pending_qty = stock_qty + if pending_qty: items.append(dict( name= i.name, @@ -384,6 +391,7 @@ class SalesOrder(SellingController): bom = bom, warehouse = i.warehouse, pending_qty = pending_qty, + required_qty = pending_qty if for_raw_material_request else 0, sales_order_item = i.name )) return items @@ -846,7 +854,7 @@ def get_supplier(doctype, txt, searchfield, start, page_len, filters): or supplier_name like %(txt)s) and name in (select supplier from `tabSales Order Item` where parent = %(parent)s) and name not in (select supplier from `tabPurchase Order` po inner join `tabPurchase Order Item` poi - on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s) + on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s) order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999), @@ -902,3 +910,44 @@ def get_default_bom_item(item_code): bom = bom[0].name if bom else None return bom + +@frappe.whitelist() +def make_raw_material_request(items, company, sales_order, project=None): + if not frappe.has_permission("Sales Order", "write"): + frappe.throw(_("Not permitted"), frappe.PermissionError) + + if isinstance(items, string_types): + items = frappe._dict(json.loads(items)) + + for item in items.get('items'): + item["include_exploded_items"] = items.get('include_exploded_items') + item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty') + + raw_materials = get_items_for_material_requests(items, company) + if not raw_materials: + frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available.")) + + material_request = frappe.new_doc('Material Request') + material_request.update(dict( + doctype = 'Material Request', + transaction_date = nowdate(), + company = company, + requested_by = frappe.session.user, + material_request_type = 'Purchase' + )) + for item in raw_materials: + item_doc = frappe.get_cached_doc('Item', item.get('item_code')) + schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) + material_request.append('items', { + 'item_code': item.get('item_code'), + 'qty': item.get('quantity'), + 'schedule_date': schedule_date, + 'warehouse': item.get('warehouse'), + 'sales_order': sales_order, + 'project': project + }) + material_request.insert() + material_request.flags.ignore_permissions = 1 + material_request.run_method("set_missing_values") + material_request.submit() + return material_request \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 538ea55737..65e91bc247 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -11,8 +11,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.selling.doctype.sales_order.sales_order import make_work_orders from erpnext.controllers.accounts_controller import update_child_qty_rate import json - - +from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request class TestSalesOrder(unittest.TestCase): def tearDown(self): frappe.set_user("Administrator") @@ -327,9 +326,8 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.CancelledLinkError, dn.submit) def test_service_type_product_bundle(self): - from erpnext.stock.doctype.item.test_item import make_item from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle - + from erpnext.stock.doctype.item.test_item import make_item make_item("_Test Service Product Bundle", {"is_stock_item": 0}) make_item("_Test Service Product Bundle Item 1", {"is_stock_item": 0}) make_item("_Test Service Product Bundle Item 2", {"is_stock_item": 0}) @@ -343,9 +341,8 @@ class TestSalesOrder(unittest.TestCase): self.assertTrue("_Test Service Product Bundle Item 2" in [d.item_code for d in so.packed_items]) def test_mix_type_product_bundle(self): - from erpnext.stock.doctype.item.test_item import make_item from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle - + from erpnext.stock.doctype.item.test_item import make_item make_item("_Test Mix Product Bundle", {"is_stock_item": 0}) make_item("_Test Mix Product Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Mix Product Bundle Item 2", {"is_stock_item": 0}) @@ -388,11 +385,10 @@ class TestSalesOrder(unittest.TestCase): def test_drop_shipping(self): from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_drop_shipment - from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.purchase_order import update_status make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - + from erpnext.stock.doctype.item.test_item import make_item po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1}) dn_item = make_item("_Test Regular Item", {"is_stock_item": 1}) @@ -585,8 +581,8 @@ class TestSalesOrder(unittest.TestCase): self.assertEquals(wo_qty[0][0], so_item_name.get(item)) def test_serial_no_based_delivery(self): - from erpnext.stock.doctype.item.test_item import make_item frappe.set_value("Stock Settings", None, "automatically_set_serial_nos_based_on_fifo", 1) + from erpnext.stock.doctype.item.test_item import make_item item = make_item("_Reserved_Serialized_Item", {"is_stock_item": 1, "maintain_stock": 1, "has_serial_no": 1, @@ -685,12 +681,62 @@ class TestSalesOrder(unittest.TestCase): se.cancel() self.assertFalse(frappe.db.exists("Serial No", {"sales_order": so.name})) + def test_request_for_raw_materials(self): + from erpnext.stock.doctype.item.test_item import make_item + item = make_item("_Test Finished Item", {"is_stock_item": 1, + "maintain_stock": 1, + "valuation_rate": 500, + "item_defaults": [ + { + "default_warehouse": "_Test Warehouse - _TC", + "company": "_Test Company" + }] + }) + make_item("_Test Raw Item A", {"maintain_stock": 1, + "valuation_rate": 100, + "item_defaults": [ + { + "default_warehouse": "_Test Warehouse - _TC", + "company": "_Test Company" + }] + }) + make_item("_Test Raw Item B", {"maintain_stock": 1, + "valuation_rate": 200, + "item_defaults": [ + { + "default_warehouse": "_Test Warehouse - _TC", + "company": "_Test Company" + }] + }) + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + make_bom(item=item.item_code, rate=1000, + raw_materials = ['_Test Raw Item A', '_Test Raw Item B']) + + so = make_sales_order(**{ + "item_list": [{ + "item_code": item.item_code, + "qty": 1, + "rate":1000 + }] + }) + so.submit() + mr_dict = frappe._dict() + items = so.get_work_order_items(1) + mr_dict['items'] = items + mr_dict['include_exploded_items'] = 0 + mr_dict['ignore_existing_ordered_qty'] = 1 + make_raw_material_request(mr_dict, so.company, so.name) + mr = frappe.db.sql("""select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1)[0] + mr_doc = frappe.get_doc('Material Request',mr.get('name')) + self.assertEqual(mr_doc.items[0].sales_order, so.name) + def make_sales_order(**args): so = frappe.new_doc("Sales Order") args = frappe._dict(args) if args.transaction_date: so.transaction_date = args.transaction_date + so.set_warehouse = "" # no need to test set_warehouse permission since it only affects the client so.company = args.company or "_Test Company" so.customer = args.customer or "_Test Customer" so.currency = args.currency or "INR" @@ -714,7 +760,7 @@ def make_sales_order(**args): }) so.delivery_date = add_days(so.transaction_date, 10) - + if not args.do_not_save: so.insert() if not args.do_not_submit: diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index ed28204f10..daec5b5a21 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -11,12 +11,9 @@ from six import string_types @frappe.whitelist() def get_items(start, page_length, price_list, item_group, search_value="", pos_profile=None): - serial_no = "" - batch_no = "" - barcode = "" + data = dict() warehouse = "" display_items_in_stock = 0 - item_code = search_value if pos_profile: warehouse, display_items_in_stock = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'display_items_in_stock']) @@ -25,20 +22,12 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p item_group = get_root_of('Item Group') if search_value: - # search serial no - serial_no_data = frappe.db.get_value('Serial No', search_value, ['name', 'item_code']) - if serial_no_data: - serial_no, item_code = serial_no_data + data = search_serial_or_batch_or_barcode_number(search_value) - if not serial_no: - batch_no_data = frappe.db.get_value('Batch', search_value, ['name', 'item']) - if batch_no_data: - batch_no, item_code = batch_no_data - - if not serial_no and not batch_no: - barcode_data = frappe.db.get_value('Item Barcode', {'barcode': search_value}, ['parent', 'barcode']) - if barcode_data: - item_code, barcode = barcode_data + item_code = data.get("item_code") if data.get("item_code") else search_value + serial_no = data.get("serial_no") if data.get("serial_no") else "" + batch_no = data.get("batch_no") if data.get("batch_no") else "" + barcode = data.get("barcode") if data.get("barcode") else "" item_code, condition = get_conditions(item_code, serial_no, batch_no, barcode) @@ -119,6 +108,23 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p return res +@frappe.whitelist() +def search_serial_or_batch_or_barcode_number(search_value): + # search barcode no + barcode_data = frappe.db.get_value('Item Barcode', {'barcode': search_value}, ['barcode', 'parent as item_code'], as_dict=True) + if barcode_data: + return barcode_data + + # search serial no + serial_no_data = frappe.db.get_value('Serial No', search_value, ['name as serial_no', 'item_code'], as_dict=True) + if serial_no_data: + return serial_no_data + + # search batch no + batch_no_data = frappe.db.get_value('Batch', search_value, ['name as batch_no', 'item as item_code'], as_dict=True) + if batch_no_data: + return batch_no_data + def get_conditions(item_code, serial_no, batch_no, barcode): if serial_no or batch_no or barcode: return frappe.db.escape(item_code), "i.name = %(item_code)s" diff --git a/erpnext/selling/report/sales_analytics/__init__.py b/erpnext/selling/report/sales_analytics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js new file mode 100644 index 0000000000..7c9e3ec35a --- /dev/null +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -0,0 +1,129 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Sales Analytics"] = { + "filters": [ + { + fieldname: "tree_type", + label: __("Tree Type"), + fieldtype: "Select", + options: ["Customer Group","Customer","Item Group","Item","Territory"], + default: "Customer", + reqd: 1 + }, + { + fieldname: "doc_type", + label: __("based_on"), + fieldtype: "Select", + options: ["Sales Order","Delivery Note","Sales Invoice"], + default: "Sales Invoice", + reqd: 1 + }, + { + fieldname: "value_quantity", + label: __("Value Or Qty"), + fieldtype: "Select", + options: [ + { "value": "Value", "label": __("Value") }, + { "value": "Quantity", "label": __("Quantity") }, + ], + default: "Value", + reqd: 1 + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_start_date"), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_end_date"), + reqd: 1 + }, + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "range", + label: __("Range"), + fieldtype: "Select", + options: [ + { "value": "Weekly", "label": __("Weekly") }, + { "value": "Monthly", "label": __("Monthly") }, + { "value": "Quarterly", "label": __("Quarterly") }, + { "value": "Yearly", "label": __("Yearly") } + ], + default: "Monthly", + reqd: 1 + } + ], + "formatter": function(value, row, column, data) { + if(!value){ + value = 0 + } + return value; + }, + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + events: { + onCheckRow: function(data) { + row_name = data[2].content; + row_values = data.slice(5).map(function (column) { + return column.content; + }) + + entry = { + 'name':row_name, + 'values':row_values + } + + let raw_data = frappe.query_report.chart.data; + let new_datasets = raw_data.datasets; + + var found = false; + + for(var i=0; i < new_datasets.length;i++){ + if(new_datasets[i].name == row_name){ + found = true; + new_datasets.splice(i,1); + break; + } + } + + if(!found){ + new_datasets.push(entry); + } + + let new_data = { + labels: raw_data.labels, + datasets: new_datasets + } + + setTimeout(() => { + frappe.query_report.chart.update(new_data) + },200) + + + setTimeout(() => { + frappe.query_report.chart.draw(true); + }, 800) + + frappe.query_report.raw_chart_data = new_data; + }, + } + }) + }, +} + + diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.json b/erpnext/selling/report/sales_analytics/sales_analytics.json new file mode 100644 index 0000000000..5c95f28410 --- /dev/null +++ b/erpnext/selling/report/sales_analytics/sales_analytics.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "creation": "2018-09-21 12:46:29.451048", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2018-09-21 12:46:29.451048", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Analytics", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Sales Analytics", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Maintenance User" + }, + { + "role": "Accounts User" + }, + { + "role": "Sales Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py new file mode 100644 index 0000000000..ef9e66656a --- /dev/null +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -0,0 +1,286 @@ +# 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 _, scrub +from frappe.utils import getdate, flt +from six import iteritems +from erpnext.accounts.utils import get_fiscal_year + +def execute(filters=None): + return Analytics(filters).run() + +class Analytics(object): + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + self.date_field = 'transaction_date' \ + if self.filters.doc_type in ['Sales Order', 'Purchase Order'] else 'posting_date' + self.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + self.get_period_date_ranges() + + def run(self): + self.get_columns() + self.get_data() + self.get_chart_data() + return self.columns, self.data , None, self.chart + + def get_columns(self): + self.columns =[{ + "label": _(self.filters.tree_type + " ID"), + "options": self.filters.tree_type, + "fieldname": "entity", + "fieldtype": "Link", + "width": 140 + }] + if self.filters.tree_type in ["Customer", "Supplier", "Item"]: + self.columns.append({ + "label": _(self.filters.tree_type + " Name"), + "fieldname": "entity_name", + "fieldtype": "Data", + "width": 140 + }) + for dummy, end_date in self.periodic_daterange: + period = self.get_period(end_date) + self.columns.append({ + "label": _(period), + "fieldname": scrub(period), + "fieldtype": "Float", + "width": 120 + }) + + self.columns.append({ + "label": _("Total"), + "fieldname": "total", + "fieldtype": "Float", + "width": 120 + }) + + def get_data(self): + if self.filters.tree_type in ["Customer", "Supplier"]: + self.get_sales_transactions_based_on_customers_or_suppliers() + self.get_rows() + + elif self.filters.tree_type == 'Item': + self.get_sales_transactions_based_on_items() + self.get_rows() + + elif self.filters.tree_type in ["Customer Group", "Supplier Group", "Territory"]: + self.get_sales_transactions_based_on_customer_or_territory_group() + self.get_rows_by_group() + + elif self.filters.tree_type == 'Item Group': + self.get_sales_transactions_based_on_item_group() + self.get_rows_by_group() + + def get_sales_transactions_based_on_customers_or_suppliers(self): + if self.filters["value_quantity"] == 'Value': + value_field = "base_net_total as value_field" + else: + value_field = "total_qty as value_field" + + if self.filters.tree_type == 'Customer': + entity = "customer as entity" + entity_name = "customer_name as entity_name" + else: + entity = "supplier as entity" + entity_name = "supplier_name as entity_name" + + self.entries = frappe.get_all(self.filters.doc_type, + fields=[entity, entity_name, value_field, self.date_field], + filters = { + "docstatus": 1, + "company": self.filters.company, + self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) + } + ) + + self.entity_names = {} + for d in self.entries: + self.entity_names.setdefault(d.entity, d.entity_name) + + def get_sales_transactions_based_on_items(self): + + if self.filters["value_quantity"] == 'Value': + value_field = 'base_amount' + else: + value_field = 'qty' + + self.entries = frappe.db.sql(""" + select i.item_code as entity, i.item_name as entity_name, i.{value_field} as value_field, s.{date_field} + from `tab{doctype} Item` i , `tab{doctype}` s + where s.name = i.parent and i.docstatus = 1 and s.company = %s + and s.{date_field} between %s and %s + """ + .format(date_field=self.date_field, value_field = value_field, doctype=self.filters.doc_type), + (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + + self.entity_names = {} + for d in self.entries: + self.entity_names.setdefault(d.entity, d.entity_name) + + def get_sales_transactions_based_on_customer_or_territory_group(self): + if self.filters["value_quantity"] == 'Value': + value_field = "base_net_total as value_field" + else: + value_field = "total_qty as value_field" + + if self.filters.tree_type == 'Customer Group': + entity_field = 'customer_group as entity' + elif self.filters.tree_type == 'Supplier Group': + entity_field = "supplier as entity" + self.get_supplier_parent_child_map() + else: + entity_field = "territory as entity" + + self.entries = frappe.get_all(self.filters.doc_type, + fields=[entity_field, value_field, self.date_field], + filters = { + "docstatus": 1, + "company": self.filters.company, + self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) + } + ) + self.get_groups() + + def get_sales_transactions_based_on_item_group(self): + if self.filters["value_quantity"] == 'Value': + value_field = "base_amount" + else: + value_field = "qty" + + self.entries = frappe.db.sql(""" + select i.item_group as entity, i.{value_field} as value_field, s.{date_field} + from `tab{doctype} Item` i , `tab{doctype}` s + where s.name = i.parent and i.docstatus = 1 and s.company = %s + and s.{date_field} between %s and %s + """.format(date_field=self.date_field, value_field = value_field, doctype=self.filters.doc_type), + (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + + self.get_groups() + + def get_rows(self): + self.data=[] + self.get_periodic_data() + + for entity, period_data in iteritems(self.entity_periodic_data): + row = { + "entity": entity, + "entity_name": self.entity_names.get(entity) + } + total = 0 + for dummy, end_date in self.periodic_daterange: + period = self.get_period(end_date) + amount = flt(period_data.get(period, 0.0)) + row[scrub(period)] = amount + total += amount + + row["total"] = total + self.data.append(row) + + def get_rows_by_group(self): + self.get_periodic_data() + out = [] + + for d in reversed(self.group_entries): + row = { + "entity": d.name, + "indent": self.depth_map.get(d.name) + } + total = 0 + for dummy, end_date in self.periodic_daterange: + period = self.get_period(end_date) + amount = flt(self.entity_periodic_data.get(d.name, {}).get(period, 0.0)) + row[scrub(period)] = amount + if d.parent: + self.entity_periodic_data.setdefault(d.parent, frappe._dict()).setdefault(period, 0.0) + self.entity_periodic_data[d.parent][period] += amount + total += amount + row["total"] = total + out = [row] + out + self.data = out + + def get_periodic_data(self): + self.entity_periodic_data = frappe._dict() + + for d in self.entries: + if self.filters.tree_type == "Supplier Group": + d.entity = self.parent_child_map.get(d.entity) + period = self.get_period(d.get(self.date_field)) + self.entity_periodic_data.setdefault(d.entity, frappe._dict()).setdefault(period, 0.0) + self.entity_periodic_data[d.entity][period] += flt(d.value_field) + + def get_period(self, posting_date): + if self.filters.range == 'Weekly': + period = "Week " + str(posting_date.isocalendar()[1]) + elif self.filters.range == 'Monthly': + period = self.months[posting_date.month - 1] + elif self.filters.range == 'Quarterly': + period = "Quarter " + str(((posting_date.month-1)//3)+1) + else: + year = get_fiscal_year(posting_date, company=self.filters.company) + period = str(year[2]) + + return period + + def get_period_date_ranges(self): + from dateutil.relativedelta import relativedelta + from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) + + increment = { + "Monthly": 1, + "Quarterly": 3, + "Half-Yearly": 6, + "Yearly": 12 + }.get(self.filters.range, 1) + + self.periodic_daterange = [] + for dummy in range(1, 53, increment): + if self.filters.range == "Weekly": + period_end_date = from_date + relativedelta(days=6) + else: + period_end_date = from_date + relativedelta(months=increment, days=-1) + + if period_end_date > to_date: + period_end_date = to_date + self.periodic_daterange.append([from_date, period_end_date]) + + from_date = period_end_date + relativedelta(days=1) + if period_end_date == to_date: + break + + def get_groups(self): + if self.filters.tree_type == "Territory": + parent = 'parent_territory' + if self.filters.tree_type == "Customer Group": + parent = 'parent_customer_group' + if self.filters.tree_type == "Item Group": + parent = 'parent_item_group' + if self.filters.tree_type == "Supplier Group": + parent = 'parent_supplier_group' + + self.depth_map = frappe._dict() + + self.group_entries = frappe.db.sql("""select name, lft, rgt , {parent} as parent + from `tab{tree}` order by lft""" + .format(tree=self.filters.tree_type, parent=parent), as_dict=1) + + for d in self.group_entries: + if d.parent: + self.depth_map.setdefault(d.name, self.depth_map.get(d.parent) + 1) + else: + self.depth_map.setdefault(d.name, 0) + + def get_supplier_parent_child_map(self): + self.parent_child_map = frappe._dict(frappe.db.sql(""" select name, supplier_group from `tabSupplier`""")) + + def get_chart_data(self): + labels = [d.get("label") for d in self.columns[3:]] + self.chart = { + "data": { + 'labels': labels, + 'datasets':[ + ] + }, + "type": "line" + } \ No newline at end of file diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py new file mode 100644 index 0000000000..c9a00fad15 --- /dev/null +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -0,0 +1,250 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +import frappe.defaults +import unittest +from erpnext.selling.report.sales_analytics.sales_analytics import execute +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + +class TestAnalytics(unittest.TestCase): + + def tearDown(self): + frappe.db.sql(""" DELETE FROM `tabSales Order` """) + + def test_by_entity(self): + create_sales_order() + + filters = { + 'doc_type': 'Sales Order', + 'range': 'Monthly', + 'to_date': '2018-03-31', + 'tree_type': 'Customer', + 'company': '_Test Company', + 'from_date': '2017-04-01', + 'value_quantity': 'Value' + } + + report = execute(filters) + + expected_data = [ + { + "entity": "_Test Customer 1", + "entity_name": "_Test Customer 1", + "apr": 0.0, + "may": 0.0, + "jun": 0.0, + "jul": 0.0, + "aug": 0.0, + "sep": 0.0, + "oct": 0.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 2000.0, + "mar": 0.0, + "total":2000.0 + }, + { + "entity": "_Test Customer 3", + "entity_name": "_Test Customer 3", + "apr": 0.0, + "may": 0.0, + "jun": 2000.0, + "jul": 1000.0, + "aug": 0.0, + "sep": 0.0, + "oct": 0.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 0.0, + "mar": 0.0, + "total": 3000.0 + }, + { + "entity": "_Test Customer 2", + "entity_name": "_Test Customer 2", + "apr": 0.0, + "may": 0.0, + "jun": 0.0, + "jul": 0.0, + "aug": 0.0, + "sep": 1500.0, + "oct": 1000.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 0.0, + "mar": 0.0, + "total":2500.0 + } + ] + self.assertEqual(expected_data, report[1]) + + def test_by_group(self): + create_sales_order() + + filters = { + 'doc_type': 'Sales Order', + 'range': 'Monthly', + 'to_date': '2018-03-31', + 'tree_type': 'Customer Group', + 'company': '_Test Company', + 'from_date': '2017-04-01', + 'value_quantity': 'Value' + } + + report = execute(filters) + + expected_data = [ + { + "entity": "All Customer Groups", + "indent": 0, + "apr": 0.0, + "may": 0.0, + "jun": 2000.0, + "jul": 1000.0, + "aug": 0.0, + "sep": 1500.0, + "oct": 1000.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 2000.0, + "mar": 0.0, + "total":7500.0 + }, + { + "entity": "Individual", + "indent": 1, + "apr": 0.0, + "may": 0.0, + "jun": 0.0, + "jul": 0.0, + "aug": 0.0, + "sep": 0.0, + "oct": 0.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 0.0, + "mar": 0.0, + "total": 0.0 + }, + { + "entity": "_Test Customer Group", + "indent": 1, + "apr": 0.0, + "may": 0.0, + "jun": 0.0, + "jul": 0.0, + "aug": 0.0, + "sep": 0.0, + "oct": 0.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 0.0, + "mar": 0.0, + "total":0.0 + }, + { + "entity": "_Test Customer Group 1", + "indent": 1, + "apr": 0.0, + "may": 0.0, + "jun": 0.0, + "jul": 0.0, + "aug": 0.0, + "sep": 0.0, + "oct": 0.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 0.0, + "mar": 0.0, + "total":0.0 + } + ] + self.assertEqual(expected_data, report[1]) + + def test_by_quantity(self): + create_sales_order() + + filters = { + 'doc_type': 'Sales Order', + 'range': 'Monthly', + 'to_date': '2018-03-31', + 'tree_type': 'Customer', + 'company': '_Test Company', + 'from_date': '2017-04-01', + 'value_quantity': 'Quantity' + } + + report = execute(filters) + + expected_data = [ + { + "entity": "_Test Customer 1", + "entity_name": "_Test Customer 1", + "apr": 0.0, + "may": 0.0, + "jun": 0.0, + "jul": 0.0, + "aug": 0.0, + "sep": 0.0, + "oct": 0.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 20.0, + "mar": 0.0, + "total":20.0 + }, + { + "entity": "_Test Customer 3", + "entity_name": "_Test Customer 3", + "apr": 0.0, + "may": 0.0, + "jun": 20.0, + "jul": 10.0, + "aug": 0.0, + "sep": 0.0, + "oct": 0.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 0.0, + "mar": 0.0, + "total": 30.0 + }, + { + "entity": "_Test Customer 2", + "entity_name": "_Test Customer 2", + "apr": 0.0, + "may": 0.0, + "jun": 0.0, + "jul": 0.0, + "aug": 0.0, + "sep": 15.0, + "oct": 10.0, + "nov": 0.0, + "dec": 0.0, + "jan": 0.0, + "feb": 0.0, + "mar": 0.0, + "total":25.0 + } + ] + self.assertEqual(expected_data, report[1]) + +def create_sales_order(): + frappe.set_user("Administrator") + + make_sales_order(qty=10, customer = "_Test Customer 1", transaction_date='2018-02-10') + make_sales_order(qty=10, customer = "_Test Customer 1", transaction_date='2018-02-15') + make_sales_order(qty=15, customer = "_Test Customer 2", transaction_date='2017-09-23') + make_sales_order(qty=10, customer = "_Test Customer 2", transaction_date='2017-10-10') + make_sales_order(qty=20, customer = "_Test Customer 3", transaction_date='2017-06-15') + make_sales_order(qty=10, customer = "_Test Customer 3", transaction_date='2017-07-10') diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 9377cad143..01f8956a82 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 1, @@ -723,6 +724,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "standard_working_hours", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Standard Working Hours", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -837,7 +870,7 @@ "label": "Create Chart Of Accounts Based On", "length": 0, "no_copy": 0, - "options": "\nStandard Template\nExisting Company", + "options": "\nStandard Template\nExisting Company", "permlevel": 0, "precision": "", "print_hide": 0, @@ -871,7 +904,7 @@ "label": "Chart Of Accounts Template", "length": 0, "no_copy": 1, - "options": "", + "options": "", "permlevel": 0, "precision": "", "print_hide": 0, @@ -1158,39 +1191,39 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "round_off_cost_center", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Round Off Cost Center", - "length": 0, - "no_copy": 0, - "options": "Cost Center", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "round_off_cost_center", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Round Off Cost Center", + "length": 0, + "no_copy": 0, + "options": "Cost Center", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, "fieldname": "write_off_account", "fieldtype": "Link", "hidden": 0, @@ -1491,7 +1524,7 @@ "collapsible": 0, "columns": 0, "depends_on": "eval:!doc.__islocal", - "fieldname": "default_deferred_expense_account", + "fieldname": "default_deferred_expense_account", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 1, @@ -1500,7 +1533,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Default Deferred Expense Account", + "label": "Default Deferred Expense Account", "length": 0, "no_copy": 1, "options": "Account", @@ -1525,7 +1558,7 @@ "collapsible": 0, "columns": 0, "depends_on": "eval:!doc.__islocal", - "fieldname": "default_payroll_payable_account", + "fieldname": "default_payroll_payable_account", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 1, @@ -1534,7 +1567,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Default Payroll Payable Account", + "label": "Default Payroll Payable Account", "length": 0, "no_copy": 1, "options": "Account", @@ -1558,20 +1591,20 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "eval:!doc.__islocal", - "fieldname": "default_expense_claim_payable_account", + "depends_on": "eval:!doc.__islocal", + "fieldname": "default_expense_claim_payable_account", "fieldtype": "Link", "hidden": 0, - "ignore_user_permissions": 1, + "ignore_user_permissions": 1, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Default Expense Claim Payable Account", + "label": "Default Expense Claim Payable Account", "length": 0, - "no_copy": 1, - "options": "Account", + "no_copy": 1, + "options": "Account", "permlevel": 0, "precision": "", "print_hide": 0, @@ -2870,8 +2903,8 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-09-13 10:00:47.915706", - "modified_by": "Administrator", + "modified": "2018-10-24 12:57:46.776452", + "modified_by": "Administrator", "module": "Setup", "name": "Company", "owner": "Administrator", diff --git a/erpnext/setup/doctype/company/company_dashboard.py b/erpnext/setup/doctype/company/company_dashboard.py index 868284a75b..61332267e7 100644 --- a/erpnext/setup/doctype/company/company_dashboard.py +++ b/erpnext/setup/doctype/company/company_dashboard.py @@ -13,7 +13,7 @@ def get_data(): 'goal_doctype_link': 'company', 'goal_field': 'base_grand_total', 'date_field': 'posting_date', - 'filter_str': 'status != "Draft"', + 'filter_str': 'docstatus = 1', 'aggregation': 'sum' }, diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 309866bbcc..0216c3ba69 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -98,7 +98,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if not value: import requests - api_url = "https://frankfurter.erpnext.org/{0}".format(transaction_date) + api_url = "https://frankfurter.app/{0}".format(transaction_date) response = requests.get(api_url, params={ "base": from_currency, "symbols": to_currency diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 9620b313ed..9a154846e5 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -21,6 +21,9 @@ frappe.ui.form.on("Delivery Note", { return (doc.docstatus==1 || doc.qty<=doc.actual_qty) ? "green" : "orange" }) + erpnext.queries.setup_queries(frm, "Warehouse", function() { + return erpnext.queries.warehouse(frm.doc); + }); erpnext.queries.setup_warehouse_query(frm); frm.set_query('project', function(doc) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 765033a31b..8c96be5e88 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, @@ -1333,6 +1334,169 @@ "set_only_once": 0, "translatable": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sec_warehouse", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "set_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Set Source Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "col_break_warehouse", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Required only for sample item.", + "fieldname": "to_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 1, + "label": "To Warehouse", + "length": 0, + "no_copy": 1, + "oldfieldname": "to_warehouse", + "oldfieldtype": "Link", + "options": "Warehouse", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "items_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Section Break", + "options": "fa fa-shopping-cart", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 }, { "allow_bulk_edit": 0, @@ -1341,8 +1505,8 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "items_section", - "fieldtype": "Section Break", + "fieldname": "scan_barcode", + "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -1350,12 +1514,11 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "", + "label": "Scan Barcode", "length": 0, "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart", "permlevel": 0, + "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -3751,73 +3914,38 @@ "set_only_once": 0, "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Required only for sample item.", - "fieldname": "to_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "To Warehouse", - "length": 0, - "no_copy": 1, - "oldfieldname": "to_warehouse", - "oldfieldtype": "Link", - "options": "Warehouse", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "excise_page", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Excise Page Number", - "length": 0, - "no_copy": 0, - "oldfieldname": "excise_page", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "excise_page", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Excise Page Number", + "length": 0, + "no_copy": 0, + "oldfieldname": "excise_page", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4168,7 +4296,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-10-06 14:25:15.326772", + "modified": "2018-11-12 20:01:34.432403", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", @@ -4282,4 +4410,4 @@ "track_changes": 1, "track_seen": 1, "track_views": 0 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 9ec2a387ec..9631264f20 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -10,5 +10,47 @@ frappe.listview_settings['Delivery Note'] = { } else if (doc.grand_total === 0 || flt(doc.per_billed, 2) == 100) { return [__("Completed"), "green", "per_billed,=,100"]; } + }, + onload: function (doclist) { + const action = () => { + const selected_docs = doclist.get_checked_items(); + const docnames = doclist.get_checked_items(true); + + if (selected_docs.length > 0) { + for (let doc of selected_docs) { + if (!doc.docstatus) { + frappe.throw(__("Cannot create a Delivery Trip from Draft documents.")); + } + }; + + frappe.new_doc("Delivery Trip") + .then(() => { + // Empty out the child table before inserting new ones + cur_frm.set_value("delivery_stops", []); + + // We don't want to use `map_current_doc` since it brings up + // the dialog to select more items. We just want the mapper + // function to be called. + frappe.call({ + type: "POST", + method: "frappe.model.mapper.map_docs", + args: { + "method": "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip", + "source_names": docnames, + "target_doc": cur_frm.doc + }, + callback: function (r) { + if (!r.exc) { + frappe.model.sync(r.message); + cur_frm.dirty(); + cur_frm.refresh(); + } + } + }); + }) + }; + }; + + doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); } }; diff --git a/erpnext/stock/doctype/delivery_stop/delivery_stop.json b/erpnext/stock/doctype/delivery_stop/delivery_stop.json index 7bce72dfde..5610a8108a 100644 --- a/erpnext/stock/doctype/delivery_stop/delivery_stop.json +++ b/erpnext/stock/doctype/delivery_stop/delivery_stop.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, @@ -173,6 +174,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.docstatus==1", + "fieldname": "visited", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Visited", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -762,7 +796,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-10-11 22:32:27.450906", + "modified": "2018-10-16 05:23:25.661542", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Stop", diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index a38c6d7a0a..a9e2f888fb 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -3,6 +3,8 @@ frappe.ui.form.on('Delivery Trip', { setup: function (frm) { + frm.set_indicator_formatter('customer', (stop) => (stop.visited) ? "green" : "orange"); + frm.set_query("driver", function () { return { filters: { diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json index a9236e88b5..1d32ecd12f 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, @@ -540,7 +541,7 @@ { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, - "allow_on_submit": 0, + "allow_on_submit": 1, "bold": 0, "collapsible": 0, "columns": 0, @@ -568,6 +569,70 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 1, + "label": "Status", + "length": 0, + "no_copy": 1, + "options": "Draft\nScheduled\nIn Transit\nCompleted\nCancelled", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_more_info", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -611,7 +676,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-10-11 22:32:04.355068", + "modified": "2018-10-22 08:25:42.323147", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Trip", @@ -663,6 +728,7 @@ "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", + "title_field": "driver_name", "track_changes": 0, "track_seen": 0, "track_views": 0 diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 431eb6c6a5..01b4734bf5 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -27,9 +27,14 @@ class DeliveryTrip(Document): self.validate_stop_addresses() def on_submit(self): + self.update_status() self.update_delivery_notes() + def on_update_after_submit(self): + self.update_status() + def on_cancel(self): + self.update_status() self.update_delivery_notes(delete=True) def validate_stop_addresses(self): @@ -37,6 +42,22 @@ class DeliveryTrip(Document): if not stop.customer_address: stop.customer_address = get_address_display(frappe.get_doc("Address", stop.address).as_dict()) + def update_status(self): + status = { + 0: "Draft", + 1: "Scheduled", + 2: "Cancelled" + }[self.docstatus] + + if self.docstatus == 1: + visited_stops = [stop.visited for stop in self.delivery_stops] + if all(visited_stops): + status = "Completed" + elif any(visited_stops): + status = "In Transit" + + self.db_set("status", status) + def update_delivery_notes(self, delete=False): """ Update all connected Delivery Notes with Delivery Trip details diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js b/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js new file mode 100644 index 0000000000..1d198b733f --- /dev/null +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings['Delivery Trip'] = { + add_fields: ["status"], + get_indicator: function (doc) { + if (in_list(["Cancelled", "Draft"], doc.status)) { + return [__(doc.status), "red", "status,=," + doc.status]; + } else if (in_list(["In Transit", "Scheduled"], doc.status)) { + return [__(doc.status), "orange", "status,=," + doc.status]; + } else if (doc.status === "Completed") { + return [__(doc.status), "green", "status,=," + doc.status]; + } + } +}; diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index b0a3d315ae..76b6fcda83 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -9,7 +9,7 @@ import erpnext import frappe from erpnext.stock.doctype.delivery_trip.delivery_trip import get_contact_and_address, notify_customers from erpnext.tests.utils import create_test_contact_and_address -from frappe.utils import add_days, now_datetime +from frappe.utils import add_days, flt, now_datetime, nowdate class TestDeliveryTrip(unittest.TestCase): @@ -72,6 +72,33 @@ class TestDeliveryTrip(unittest.TestCase): self.assertEqual(len(route_list[0]), 2) # [home_address, locked_stop] self.assertEqual(len(route_list[1]), 3) # [locked_stop, second_stop, home_address] + def test_delivery_trip_status_draft(self): + self.assertEqual(self.delivery_trip.status, "Draft") + + def test_delivery_trip_status_scheduled(self): + self.delivery_trip.submit() + self.assertEqual(self.delivery_trip.status, "Scheduled") + + def test_delivery_trip_status_cancelled(self): + self.delivery_trip.submit() + self.delivery_trip.cancel() + self.assertEqual(self.delivery_trip.status, "Cancelled") + + def test_delivery_trip_status_in_transit(self): + self.delivery_trip.submit() + self.delivery_trip.delivery_stops[0].visited = 1 + self.delivery_trip.save() + self.assertEqual(self.delivery_trip.status, "In Transit") + + def test_delivery_trip_status_completed(self): + self.delivery_trip.submit() + + for stop in self.delivery_trip.delivery_stops: + stop.visited = 1 + + self.delivery_trip.save() + self.assertEqual(self.delivery_trip.status, "Completed") + def create_driver(): if not frappe.db.exists("Driver", "Newton Scmander"): @@ -108,11 +135,11 @@ def create_vehicle(): "make": "Maruti", "model": "PCM", "last_odometer": 5000, - "acquisition_date": frappe.utils.nowdate(), + "acquisition_date": nowdate(), "location": "Mumbai", "chassis_no": "1234ABCD", "uom": "Litre", - "vehicle_value": frappe.utils.flt(500000) + "vehicle_value": flt(500000) }) vehicle.insert() diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index c0285cbe14..8a53a27314 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, @@ -312,6 +313,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scan_barcode", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scan Barcode", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 1, "allow_in_quick_entry": 0, @@ -826,7 +859,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-09-05 07:28:01.070112", + "modified": "2018-10-18 04:41:56.818108", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 736ba8bb8d..1d9eb9965b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -21,26 +21,9 @@ frappe.ui.form.on("Purchase Receipt", { }) }, onload: function(frm) { - $.each(["warehouse", "rejected_warehouse"], function(i, field) { - frm.set_query(field, "items", function() { - return { - filters: [ - ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], - ["Warehouse", "is_group", "=", 0] - ] - } - }) - }) - - frm.set_query("supplier_warehouse", function() { - return { - filters: [ - ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], - ["Warehouse", "is_group", "=", 0] - ] - } + erpnext.queries.setup_queries(frm, "Warehouse", function() { + return erpnext.queries.warehouse(frm.doc); }); - }, onload_post_render: function(frm) { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index f2da856d42..3e9ff4997d 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, @@ -1071,6 +1072,211 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sec_warehouse", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "set_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Set Accepted Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Warehouse where you are maintaining stock of rejected items", + "fieldname": "rejected_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Rejected Warehouse", + "length": 0, + "no_copy": 1, + "oldfieldname": "rejected_warehouse", + "oldfieldtype": "Link", + "options": "Warehouse", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "col_break_warehouse", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "No", + "description": "", + "fieldname": "is_subcontracted", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Raw Materials Supplied", + "length": 0, + "no_copy": 0, + "oldfieldname": "is_subcontracted", + "oldfieldtype": "Select", + "options": "No\nYes", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.is_subcontracted==\"Yes\"", + "description": "", + "fieldname": "supplier_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Supplier Warehouse", + "length": 0, + "no_copy": 1, + "oldfieldname": "supplier_warehouse", + "oldfieldtype": "Link", + "options": "Warehouse", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0, + "width": "50px" + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1171,6 +1377,75 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "supplied_items", + "columns": 0, + "description": "", + "fieldname": "raw_material_details", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Raw Materials Supplied", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Section Break", + "options": "fa fa-table", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "supplied_items", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Supplied Items", + "length": 0, + "no_copy": 1, + "oldfieldname": "pr_raw_material_details", + "oldfieldtype": "Table", + "options": "Purchase Receipt Item Supplied", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -2623,148 +2898,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "supplied_items", - "columns": 0, - "description": "", - "fieldname": "raw_material_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Materials Supplied", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-table", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "No", - "description": "", - "fieldname": "is_subcontracted", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Materials Supplied", - "length": 0, - "no_copy": 0, - "oldfieldname": "is_subcontracted", - "oldfieldtype": "Select", - "options": "No\nYes", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "supplier_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplier Warehouse", - "length": 0, - "no_copy": 1, - "oldfieldname": "supplier_warehouse", - "oldfieldtype": "Link", - "options": "Warehouse", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, - "width": "50px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplied_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplied Items", - "length": 0, - "no_copy": 1, - "oldfieldname": "pr_raw_material_details", - "oldfieldtype": "Table", - "options": "Purchase Receipt Item Supplied", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -2901,41 +3034,6 @@ "unique": 0, "width": "150px" }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Warehouse where you are maintaining stock of rejected items", - "fieldname": "rejected_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Rejected Warehouse", - "length": 0, - "no_copy": 1, - "oldfieldname": "rejected_warehouse", - "oldfieldtype": "Link", - "options": "Warehouse", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -3610,7 +3708,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-08-21 14:44:34.419727", + "modified": "2018-11-02 19:59:01.423485", "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 e482f58da3..f006d00176 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -185,7 +185,8 @@ class PurchaseReceipt(BuyingController): if warehouse_account.get(d.warehouse): stock_value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", "voucher_no": self.name, - "voucher_detail_no": d.name}, "stock_value_difference") + "voucher_detail_no": d.name, "warehouse": d.warehouse}, "stock_value_difference") + if not stock_value_diff: continue gl_entries.append(self.get_gl_dict({ diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 0356b0e14f..a26992a5ae 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -594,6 +594,11 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ erpnext.utils.add_item(this.frm); }, + scan_barcode: function() { + let transaction_controller= new erpnext.TransactionController({frm:this.frm}); + transaction_controller.scan_barcode(); + }, + on_submit: function() { this.clean_up(); }, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 35f8c27344..6a925adef1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, @@ -1044,6 +1045,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scan_barcode", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scan Barcode", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1965,7 +1998,7 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0, + "set_only_once": 0, "translatable": 0, "unique": 0 }, @@ -2048,7 +2081,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-09-05 06:27:59.630826", + "modified": "2018-10-18 04:42:41.452572", "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 d4e7b9574f..d8e46563f6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -634,19 +634,19 @@ class StockEntry(StockController): ret = frappe._dict({ 'uom' : item.stock_uom, - 'stock_uom' : item.stock_uom, + 'stock_uom' : item.stock_uom, 'description' : item.description, - 'image' : item.image, + 'image' : item.image, 'item_name' : item.item_name, 'expense_account' : args.get("expense_account"), 'cost_center' : get_default_cost_center(args, item, item_group_defaults), - 'qty' : 0, - 'transfer_qty' : 0, + 'qty' : args.get("qty"), + 'transfer_qty' : args.get('qty'), 'conversion_factor' : 1, - 'batch_no' : '', + 'batch_no' : '', 'actual_qty' : 0, 'basic_rate' : 0, - 'serial_no' : '', + 'serial_no' : '', 'has_serial_no' : item.has_serial_no, 'has_batch_no' : item.has_batch_no, 'sample_quantity' : item.sample_quantity diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 97d24c6c94..65de2e58d3 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -26,8 +26,9 @@ class StockSettings(Document): frappe.msgprint (_("`Freeze Stocks Older Than` should be smaller than %d days.") %stock_frozen_limit) # show/hide barcode field - frappe.make_property_setter({'fieldname': 'barcodes', 'property': 'hidden', - 'value': 0 if self.show_barcode_field else 1}) + for name in ["barcode", "barcodes", "scan_barcode"]: + frappe.make_property_setter({'fieldname': name, 'property': 'hidden', + 'value': 0 if self.show_barcode_field else 1}) self.cant_change_valuation_method() self.validate_clean_description_html() diff --git a/erpnext/stock/report/stock_analytics/__init__.py b/erpnext/stock/report/stock_analytics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.js b/erpnext/stock/report/stock_analytics/stock_analytics.js new file mode 100644 index 0000000000..6010ea9ee2 --- /dev/null +++ b/erpnext/stock/report/stock_analytics/stock_analytics.js @@ -0,0 +1,136 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Stock Analytics"] = { + "filters": [ + { + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options:"Item Group", + default: "", + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options:"Item", + default: "", + }, + { + fieldname: "value_quantity", + label: __("Value Or Qty"), + fieldtype: "Select", + options: [ + { "value": "Value", "label": __("Value") }, + { "value": "Quantity", "label": __("Quantity") } + ], + default: "Value", + reqd: 1 + }, + { + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options:"Brand", + default: "", + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options:"Warehouse", + default: "", + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_start_date"), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_end_date"), + reqd: 1 + }, + { + fieldname: "range", + label: __("Range"), + fieldtype: "Select", + options: [ + { "value": "Weekly", "label": __("Weekly") }, + { "value": "Monthly", "label": __("Monthly") }, + { "value": "Quarterly", "label": __("Quarterly") }, + { "value": "Yearly", "label": __("Yearly") } + ], + default: "Monthly", + reqd: 1 + } + ], + "formatter": function(value, row, column, data) { + if(!value && (column.fieldname == 'brand' || column.fieldname == 'uom')){ + value = "" + } + + if(Number(value)){ + value = value.toFixed(2) + } + + return value; + }, + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + events: { + onCheckRow: function(data) { + row_name = data[2].content; + row_values = data.slice(6).map(function (column) { + return column.content; + }) + + entry = { + 'name':row_name, + 'values':row_values + } + + let raw_data = frappe.query_report.chart.data; + let new_datasets = raw_data.datasets; + + var found = false; + + for(var i=0; i < new_datasets.length;i++){ + if(new_datasets[i].name == row_name){ + found = true; + new_datasets.splice(i,1); + break; + } + } + + if(!found){ + new_datasets.push(entry); + } + + let new_data = { + labels: raw_data.labels, + datasets: new_datasets + } + + setTimeout(() => { + frappe.query_report.chart.update(new_data) + },200) + + + setTimeout(() => { + frappe.query_report.chart.draw(true); + }, 800) + + frappe.query_report.raw_chart_data = new_data; + }, + } + }) + }, +} diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.json b/erpnext/stock/report/stock_analytics/stock_analytics.json new file mode 100644 index 0000000000..efd5e99cbc --- /dev/null +++ b/erpnext/stock/report/stock_analytics/stock_analytics.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "creation": "2018-10-08 12:11:32.133020", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2018-10-08 12:18:42.834270", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Analytics", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Entry", + "report_name": "Stock Analytics", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing Manager" + }, + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + }, + { + "role": "Manufacturing User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py new file mode 100644 index 0000000000..5a8a672b63 --- /dev/null +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -0,0 +1,185 @@ +# 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 _, scrub +from frappe.utils import getdate, flt +from erpnext.stock.report.stock_balance.stock_balance import (get_items, get_stock_ledger_entries, get_item_details) +from erpnext.accounts.utils import get_fiscal_year +from six import iteritems + +def execute(filters=None): + filters = frappe._dict(filters or {}) + columns = get_columns(filters) + data = get_data(filters) + chart = get_chart_data(columns) + + return columns, data, None, chart + +def get_columns(filters): + columns = [ + { + "label": _("Item"), + "options":"Item", + "fieldname": "name", + "fieldtype": "Link", + "width": 140 + }, + { + "label": _("Item Name"), + "options":"Item", + "fieldname": "item_name", + "fieldtype": "Link", + "width": 140 + }, + { + "label": _("Item Group"), + "options":"Item Group", + "fieldname": "item_group", + "fieldtype": "Link", + "width": 140 + }, + { + "label": _("Brand"), + "fieldname": "brand", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("UOM"), + "fieldname": "uom", + "fieldtype": "Data", + "width": 120 + }] + + ranges = get_period_date_ranges(filters) + + for dummy, end_date in ranges: + period = get_period(end_date, filters) + + columns.append({ + "label": _(period), + "fieldname":scrub(period), + "fieldtype": "Float", + "width": 120 + }) + + return columns + +def get_period_date_ranges(filters): + from dateutil.relativedelta import relativedelta + from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) + + increment = { + "Monthly": 1, + "Quarterly": 3, + "Half-Yearly": 6, + "Yearly": 12 + }.get(filters.range,1) + + periodic_daterange = [] + for dummy in range(1, 53, increment): + if filters.range == "Weekly": + period_end_date = from_date + relativedelta(days=6) + else: + period_end_date = from_date + relativedelta(months=increment, days=-1) + + if period_end_date > to_date: + period_end_date = to_date + periodic_daterange.append([from_date, period_end_date]) + + from_date = period_end_date + relativedelta(days=1) + if period_end_date == to_date: + break + + return periodic_daterange + +def get_period(posting_date, filters): + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + + if filters.range == 'Weekly': + period = "Week " + str(posting_date.isocalendar()[1]) + elif filters.range == 'Monthly': + period = months[posting_date.month - 1] + elif filters.range == 'Quarterly': + period = "Quarter " + str(((posting_date.month-1)//3)+1) + else: + year = get_fiscal_year(posting_date, company=filters.company) + period = str(year[2]) + + return period + + +def get_periodic_data(entry, filters): + periodic_data = {} + for d in entry: + period = get_period(d.posting_date, filters) + bal_qty = 0 + + if d.voucher_type == "Stock Reconciliation": + if periodic_data.get(d.item_code): + bal_qty = periodic_data[d.item_code]["balance"] + + qty_diff = d.qty_after_transaction - bal_qty + else: + qty_diff = d.actual_qty + + if filters["value_quantity"] == 'Quantity': + value = qty_diff + else: + value = d.stock_value_difference + + periodic_data.setdefault(d.item_code, {}).setdefault(period, 0.0) + periodic_data.setdefault(d.item_code, {}).setdefault("balance", 0.0) + + periodic_data[d.item_code]["balance"] += value + periodic_data[d.item_code][period] = periodic_data[d.item_code]["balance"] + + + return periodic_data + +def get_data(filters): + data = [] + items = get_items(filters) + sle = get_stock_ledger_entries(filters, items) + item_details = get_item_details(items, sle, filters) + periodic_data = get_periodic_data(sle, filters) + ranges = get_period_date_ranges(filters) + + for dummy, item_data in iteritems(item_details): + row = { + "name": item_data.name, + "item_name": item_data.item_name, + "item_group": item_data.item_group, + "uom": item_data.stock_uom, + "brand": item_data.brand, + } + total = 0 + for dummy, end_date in ranges: + period = get_period(end_date, filters) + amount = flt(periodic_data.get(item_data.name, {}).get(period)) + row[scrub(period)] = amount + total += amount + row["total"] = total + data.append(row) + + return data + +def get_chart_data(columns): + labels = [d.get("label") for d in columns[4:]] + chart = { + "data": { + 'labels': labels, + 'datasets':[ + { "values": ['0' for d in columns[4:]] } + ] + } + } + chart["type"] = "line" + + return chart + + + + diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 6f984a501b..a2867c8806 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -108,8 +108,8 @@ def get_price(item_code, price_list, customer_group, company, qty=1): uom_conversion_factor = frappe.db.sql("""select C.conversion_factor from `tabUOM Conversion Detail` C - inner join `tabItem` I on C.uom = I.sales_uom - where C.parent = %s""", item_code) + inner join `tabItem` I on C.parent = I.name and C.uom = I.sales_uom + where I.name = %s""", item_code) uom_conversion_factor = uom_conversion_factor[0][0] if uom_conversion_factor else 1 price_obj["formatted_price_sales_uom"] = fmt_money(price_obj["price_list_rate"] * uom_conversion_factor, currency=price_obj["currency"])