Merge branch 'develop' of github.com:frappe/erpnext into feature-pick-list

This commit is contained in:
Suraj Shetty 2019-07-23 13:29:41 +05:30
commit ed4f8cf65d
142 changed files with 16532 additions and 9000 deletions

View File

@ -48,7 +48,10 @@ class BankAccount(Document):
# Encode characters as numbers # Encode characters as numbers
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped] encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
to_check = int(''.join(encoded)) try:
to_check = int(''.join(encoded))
except ValueError:
frappe.throw(_('IBAN is not valid'))
if to_check % 97 != 1: if to_check % 97 != 1:
frappe.throw(_('IBAN is not valid')) frappe.throw(_('IBAN is not valid'))

View File

@ -223,7 +223,10 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
if(in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) { if(in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) {
out.filters.push([jvd.reference_type, "outstanding_amount", "!=", 0]); out.filters.push([jvd.reference_type, "outstanding_amount", "!=", 0]);
// Filter by cost center
if(jvd.cost_center) {
out.filters.push([jvd.reference_type, "cost_center", "in", ["", jvd.cost_center]]);
}
// account filter // account filter
frappe.model.validate_missing(jvd, "account"); frappe.model.validate_missing(jvd, "account");
var party_account_field = jvd.reference_type==="Sales Invoice" ? "debit_to": "credit_to"; var party_account_field = jvd.reference_type==="Sales Invoice" ? "debit_to": "credit_to";

View File

@ -1,751 +1,166 @@
{ {
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-03-24 14:48:59.649168", "creation": "2019-03-24 14:48:59.649168",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"disable",
"column_break_2",
"rule_description",
"section_break_1",
"min_qty",
"max_qty",
"column_break_3",
"min_amount",
"max_amount",
"section_break_6",
"same_item",
"free_item",
"free_qty",
"column_break_9",
"free_item_uom",
"free_item_rate",
"section_break_12",
"warehouse",
"threshold_percentage",
"column_break_15",
"priority",
"apply_multiple_pricing_rules"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "disable", "fieldname": "disable",
"fieldtype": "Data", "fieldtype": "Check",
"hidden": 0, "label": "Disable"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disable",
"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": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break", "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,
"fieldname": "rule_description", "fieldname": "rule_description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"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": "Rule Description", "label": "Rule Description",
"length": 0,
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"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": "section_break_1", "fieldname": "section_break_1",
"fieldtype": "Section Break", "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,
"default": "0", "default": "0",
"fieldname": "min_qty", "fieldname": "min_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Min Qty"
"label": "Min Qty",
"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": "0", "default": "0",
"fieldname": "max_qty", "fieldname": "max_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Max Qty"
"label": "Max Qty",
"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": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "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": "0", "default": "0",
"fieldname": "min_amount", "fieldname": "min_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Min Amount"
"label": "Min Amount",
"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": "0", "default": "0",
"fieldname": "max_amount", "fieldname": "max_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Max Amount"
"label": "Max Amount",
"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": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Free Item"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Free Item",
"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, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:!parent.mixed_conditions", "depends_on": "eval:!parent.mixed_conditions",
"fieldname": "same_item", "fieldname": "same_item",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Same Item"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Same Item",
"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.same_item || parent.mixed_conditions", "depends_on": "eval:!doc.same_item || parent.mixed_conditions",
"fieldname": "free_item", "fieldname": "free_item",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code", "label": "Item Code",
"length": 0, "options": "Item"
"no_copy": 0,
"options": "Item",
"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": "free_qty", "fieldname": "free_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Qty"
"label": "Qty",
"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": "column_break_9", "fieldname": "column_break_9",
"fieldtype": "Column Break", "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,
"fieldname": "free_item_uom", "fieldname": "free_item_uom",
"fieldtype": "Link", "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": "UOM", "label": "UOM",
"length": 0, "options": "UOM"
"no_copy": 0,
"options": "UOM",
"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": "free_item_rate", "fieldname": "free_item_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0, "label": "Rate"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rate",
"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": "section_break_12", "fieldname": "section_break_12",
"fieldtype": "Section Break", "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": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "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": "Warehouse", "label": "Warehouse",
"length": 0, "options": "Warehouse"
"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,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "threshold_percentage", "fieldname": "threshold_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"hidden": 0, "label": "Threshold for Suggestion"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Threshold for Suggestion",
"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": "column_break_15", "fieldname": "column_break_15",
"fieldtype": "Column Break", "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,
"fieldname": "priority", "fieldname": "priority",
"fieldtype": "Select", "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": "Priority", "label": "Priority",
"length": 0, "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20"
"no_copy": 0,
"options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20",
"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, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "apply_multiple_pricing_rules", "fieldname": "apply_multiple_pricing_rules",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Apply Multiple Pricing Rules"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Apply Multiple Pricing Rules",
"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
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "modified": "2019-07-21 00:00:56.674284",
"modified": "2019-03-24 14:48:59.649168",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Promotional Scheme Product Discount", "name": "Promotional Scheme Product Discount",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC"
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@ -336,6 +336,7 @@ class PurchaseInvoice(BuyingController):
if not self.is_return: if not self.is_return:
self.update_against_document_in_jv() self.update_against_document_in_jv()
self.update_billing_status_for_zero_amount_refdoc("Purchase Receipt")
self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
self.update_billing_status_in_pr() self.update_billing_status_in_pr()
@ -774,6 +775,7 @@ class PurchaseInvoice(BuyingController):
self.update_prevdoc_status() self.update_prevdoc_status()
if not self.is_return: if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Purchase Receipt")
self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
self.update_billing_status_in_pr() self.update_billing_status_in_pr()

View File

@ -451,6 +451,10 @@ def make_customer_and_address(customers):
def add_customer(data): def add_customer(data):
customer = data.get('full_name') or data.get('customer')
if frappe.db.exists("Customer", customer.strip()):
return customer.strip()
customer_doc = frappe.new_doc('Customer') customer_doc = frappe.new_doc('Customer')
customer_doc.customer_name = data.get('full_name') or data.get('customer') customer_doc.customer_name = data.get('full_name') or data.get('customer')
customer_doc.customer_pos_id = data.get('customer_pos_id') customer_doc.customer_pos_id = data.get('customer_pos_id')

View File

@ -166,6 +166,7 @@ class SalesInvoice(SellingController):
self.make_gl_entries() self.make_gl_entries()
if not self.is_return: if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit() self.check_credit_limit()
@ -220,6 +221,7 @@ class SalesInvoice(SellingController):
self.update_billing_status_in_dn() self.update_billing_status_in_dn()
if not self.is_return: if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.update_serial_no(in_cancel=True) self.update_serial_no(in_cancel=True)
@ -395,14 +397,17 @@ class SalesInvoice(SellingController):
if pos.get('account_for_change_amount'): if pos.get('account_for_change_amount'):
self.account_for_change_amount = pos.get('account_for_change_amount') self.account_for_change_amount = pos.get('account_for_change_amount')
for fieldname in ('territory', 'naming_series', 'currency', 'taxes_and_charges', 'letter_head', 'tc_name', for fieldname in ('territory', 'naming_series', 'currency', 'letter_head', 'tc_name',
'company', 'select_print_heading', 'cash_bank_account', 'company_address', 'company', 'select_print_heading', 'cash_bank_account', 'write_off_account', 'taxes_and_charges',
'write_off_account', 'write_off_cost_center', 'apply_discount_on', 'cost_center'): 'write_off_cost_center', 'apply_discount_on', 'cost_center'):
if (not for_validate) or (for_validate and not self.get(fieldname)): if (not for_validate) or (for_validate and not self.get(fieldname)):
self.set(fieldname, pos.get(fieldname)) self.set(fieldname, pos.get(fieldname))
customer_price_list = frappe.get_value("Customer", self.customer, 'default_price_list') customer_price_list = frappe.get_value("Customer", self.customer, 'default_price_list')
if pos.get("company_address"):
self.company_address = pos.get("company_address")
if not customer_price_list: if not customer_price_list:
self.set('selling_price_list', pos.get('selling_price_list')) self.set('selling_price_list', pos.get('selling_price_list'))
@ -1255,9 +1260,8 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc
frappe.throw(_("Invalid Company for Inter Company Transaction.")) frappe.throw(_("Invalid Company for Inter Company Transaction."))
elif frappe.db.get_value(partytype, {"name": party, internal: 1}, "name") == party: elif frappe.db.get_value(partytype, {"name": party, internal: 1}, "name") == party:
companies = frappe.db.sql("""select company from `tabAllowed To Transact With` companies = frappe.get_all("Allowed To Transact With", fields=["company"], filters={"parenttype": partytype, "parent": party})
where parenttype = '{0}' and parent = '{1}'""".format(partytype, party), as_list = 1) companies = [d.company for d in companies]
companies = [d[0] for d in companies]
if not company in companies: if not company in companies:
frappe.throw(_("{0} not allowed to transact with {1}. Please change the Company.").format(partytype, company)) frappe.throw(_("{0} not allowed to transact with {1}. Please change the Company.").format(partytype, company))

View File

@ -12,11 +12,11 @@ def execute(filters=None):
columns = get_columns() columns = get_columns()
if not filters.get("account"): return columns, [] if not filters.get("account"): return columns, []
account_currency = frappe.db.get_value("Account", filters.account, "account_currency") account_currency = frappe.db.get_value("Account", filters.account, "account_currency")
data = get_entries(filters) data = get_entries(filters)
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
@ -24,7 +24,7 @@ def execute(filters=None):
for d in data: for d in data:
total_debit += flt(d.debit) total_debit += flt(d.debit)
total_credit += flt(d.credit) total_credit += flt(d.credit)
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters) amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \ bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \
@ -39,7 +39,7 @@ def execute(filters=None):
"credit": total_credit, "credit": total_credit,
"account_currency": account_currency "account_currency": account_currency
}, },
get_balance_row(_("Cheques and Deposits incorrectly cleared"), amounts_not_reflected_in_system, get_balance_row(_("Cheques and Deposits incorrectly cleared"), amounts_not_reflected_in_system,
account_currency), account_currency),
{}, {},
get_balance_row(_("Calculated Bank Statement balance"), bank_bal, account_currency) get_balance_row(_("Calculated Bank Statement balance"), bank_bal, account_currency)
@ -55,9 +55,16 @@ def get_columns():
"fieldtype": "Date", "fieldtype": "Date",
"width": 90 "width": 90
}, },
{
"fieldname": "payment_document",
"label": _("Payment Document Type"),
"fieldtype": "Link",
"options": "DocType",
"width": 220
},
{ {
"fieldname": "payment_entry", "fieldname": "payment_entry",
"label": _("Payment Entry"), "label": _("Payment Document"),
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"options": "payment_document", "options": "payment_document",
"width": 220 "width": 220
@ -100,7 +107,7 @@ def get_columns():
"label": _("Clearance Date"), "label": _("Clearance Date"),
"fieldtype": "Date", "fieldtype": "Date",
"width": 110 "width": 110
}, },
{ {
"fieldname": "account_currency", "fieldname": "account_currency",
"label": _("Currency"), "label": _("Currency"),
@ -112,9 +119,9 @@ def get_columns():
def get_entries(filters): def get_entries(filters):
journal_entries = frappe.db.sql(""" journal_entries = frappe.db.sql("""
select "Journal Entry" as payment_document, jv.posting_date, select "Journal Entry" as payment_document, jv.posting_date,
jv.name as payment_entry, jvd.debit_in_account_currency as debit, jv.name as payment_entry, jvd.debit_in_account_currency as debit,
jvd.credit_in_account_currency as credit, jvd.against_account, jvd.credit_in_account_currency as credit, jvd.against_account,
jv.cheque_no as reference_no, jv.cheque_date as ref_date, jv.clearance_date, jvd.account_currency jv.cheque_no as reference_no, jv.cheque_date as ref_date, jv.clearance_date, jvd.account_currency
from from
`tabJournal Entry Account` jvd, `tabJournal Entry` jv `tabJournal Entry Account` jvd, `tabJournal Entry` jv
@ -122,13 +129,13 @@ def get_entries(filters):
and jvd.account = %(account)s and jv.posting_date <= %(report_date)s and jvd.account = %(account)s and jv.posting_date <= %(report_date)s
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1) and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1)
payment_entries = frappe.db.sql(""" payment_entries = frappe.db.sql("""
select select
"Payment Entry" as payment_document, name as payment_entry, "Payment Entry" as payment_document, name as payment_entry,
reference_no, reference_date as ref_date, reference_no, reference_date as ref_date,
if(paid_to=%(account)s, received_amount, 0) as debit, if(paid_to=%(account)s, received_amount, 0) as debit,
if(paid_from=%(account)s, paid_amount, 0) as credit, if(paid_from=%(account)s, paid_amount, 0) as credit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date, posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry` from `tabPayment Entry`
@ -156,25 +163,25 @@ def get_entries(filters):
return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)),
key=lambda k: k['posting_date'] or getdate(nowdate())) key=lambda k: k['posting_date'] or getdate(nowdate()))
def get_amounts_not_reflected_in_system(filters): def get_amounts_not_reflected_in_system(filters):
je_amount = frappe.db.sql(""" je_amount = frappe.db.sql("""
select sum(jvd.debit_in_account_currency - jvd.credit_in_account_currency) select sum(jvd.debit_in_account_currency - jvd.credit_in_account_currency)
from `tabJournal Entry Account` jvd, `tabJournal Entry` jv from `tabJournal Entry Account` jvd, `tabJournal Entry` jv
where jvd.parent = jv.name and jv.docstatus=1 and jvd.account=%(account)s where jvd.parent = jv.name and jv.docstatus=1 and jvd.account=%(account)s
and jv.posting_date > %(report_date)s and jv.clearance_date <= %(report_date)s and jv.posting_date > %(report_date)s and jv.clearance_date <= %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No' """, filters) and ifnull(jv.is_opening, 'No') = 'No' """, filters)
je_amount = flt(je_amount[0][0]) if je_amount else 0.0 je_amount = flt(je_amount[0][0]) if je_amount else 0.0
pe_amount = frappe.db.sql(""" pe_amount = frappe.db.sql("""
select sum(if(paid_from=%(account)s, paid_amount, received_amount)) select sum(if(paid_from=%(account)s, paid_amount, received_amount))
from `tabPayment Entry` from `tabPayment Entry`
where (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1 where (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date > %(report_date)s and clearance_date <= %(report_date)s""", filters) and posting_date > %(report_date)s and clearance_date <= %(report_date)s""", filters)
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0 pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
return je_amount + pe_amount return je_amount + pe_amount
def get_balance_row(label, amount, account_currency): def get_balance_row(label, amount, account_currency):

View File

@ -125,8 +125,9 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
data["total"] = total data["total"] = total
return data return data
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters): def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters={}):
cond = "" cond = ""
filters = frappe._dict(filters)
if filters.finance_book: if filters.finance_book:
cond = " and finance_book = %s" %(frappe.db.escape(filters.finance_book)) cond = " and finance_book = %s" %(frappe.db.escape(filters.finance_book))
@ -187,7 +188,7 @@ def get_chart_data(columns, data):
}, },
"type": "bar" "type": "bar"
} }
chart["fieldtype"] = "Currency" chart["fieldtype"] = "Currency"
return chart return chart

View File

@ -130,7 +130,7 @@ def get_cash_flow_data(fiscal_year, companies, filters):
section_data.append(net_profit_loss) section_data.append(net_profit_loss)
for account in cash_flow_account['account_types']: for account in cash_flow_account['account_types']:
account_data = get_account_type_based_data(account['account_type'], companies, fiscal_year) account_data = get_account_type_based_data(account['account_type'], companies, fiscal_year, filters)
account_data.update({ account_data.update({
"account_name": account['label'], "account_name": account['label'],
"account": account['label'], "account": account['label'],
@ -148,12 +148,12 @@ def get_cash_flow_data(fiscal_year, companies, filters):
return data return data
def get_account_type_based_data(account_type, companies, fiscal_year): def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data = {} data = {}
total = 0 total = 0
for company in companies: for company in companies:
amount = get_account_type_based_gl_data(company, amount = get_account_type_based_gl_data(company,
fiscal_year.year_start_date, fiscal_year.year_end_date, account_type) fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters)
if amount and account_type == "Depreciation": if amount and account_type == "Depreciation":
amount *= -1 amount *= -1

View File

@ -121,7 +121,12 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company
allow_cost_center_in_entry_of_bs_account = get_allow_cost_center_in_entry_of_bs_account() allow_cost_center_in_entry_of_bs_account = get_allow_cost_center_in_entry_of_bs_account()
if cost_center and (allow_cost_center_in_entry_of_bs_account or acc.report_type =='Profit and Loss'): if account:
report_type = acc.report_type
else:
report_type = ""
if cost_center and (allow_cost_center_in_entry_of_bs_account or report_type =='Profit and Loss'):
cc = frappe.get_doc("Cost Center", cost_center) cc = frappe.get_doc("Cost Center", cost_center)
if cc.is_group: if cc.is_group:
cond.append(""" exists ( cond.append(""" exists (
@ -138,7 +143,7 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company
if not frappe.flags.ignore_account_permission: if not frappe.flags.ignore_account_permission:
acc.check_permission("read") acc.check_permission("read")
if acc.report_type == 'Profit and Loss': if report_type == 'Profit and Loss':
# for pl accounts, get balance within a fiscal year # for pl accounts, get balance within a fiscal year
cond.append("posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" \ cond.append("posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" \
% year_start_date) % year_start_date)
@ -685,7 +690,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0) payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
outstanding_amount = flt(d.invoice_amount - payment_amount, precision) outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
if outstanding_amount > 0.5 / (10**precision): if outstanding_amount > 0.5 / (10**precision):
if (filters.get("outstanding_amt_greater_than") and if (filters and filters.get("outstanding_amt_greater_than") and
not (outstanding_amount >= filters.get("outstanding_amt_greater_than") and not (outstanding_amount >= filters.get("outstanding_amt_greater_than") and
outstanding_amount <= filters.get("outstanding_amt_less_than"))): outstanding_amount <= filters.get("outstanding_amt_less_than"))):
continue continue
@ -730,7 +735,6 @@ def get_children(doctype, parent, company, is_root=False):
parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_') parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_')
fields = [ fields = [
'name as value', 'name as value',
'root_type',
'is_group as expandable' 'is_group as expandable'
] ]
filters = [['docstatus', '<', 2]] filters = [['docstatus', '<', 2]]
@ -738,11 +742,11 @@ def get_children(doctype, parent, company, is_root=False):
filters.append(['ifnull(`{0}`,"")'.format(parent_fieldname), '=', '' if is_root else parent]) filters.append(['ifnull(`{0}`,"")'.format(parent_fieldname), '=', '' if is_root else parent])
if is_root: if is_root:
fields += ['report_type', 'account_currency'] if doctype == 'Account' else [] fields += ['root_type', 'report_type', 'account_currency'] if doctype == 'Account' else []
filters.append(['company', '=', company]) filters.append(['company', '=', company])
else: else:
fields += ['account_currency'] if doctype == 'Account' else [] fields += ['root_type', 'account_currency'] if doctype == 'Account' else []
fields += [parent_fieldname + ' as parent'] fields += [parent_fieldname + ' as parent']
acc = frappe.get_list(doctype, fields=fields, filters=filters) acc = frappe.get_list(doctype, fields=fields, filters=filters)

View File

@ -388,7 +388,8 @@ class Asset(AccountsController):
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date, "posting_date": self.available_for_use_date,
"credit": self.purchase_receipt_amount, "credit": self.purchase_receipt_amount,
"credit_in_account_currency": self.purchase_receipt_amount "credit_in_account_currency": self.purchase_receipt_amount,
"cost_center": self.cost_center
})) }))
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
@ -397,7 +398,8 @@ class Asset(AccountsController):
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date, "posting_date": self.available_for_use_date,
"debit": self.purchase_receipt_amount, "debit": self.purchase_receipt_amount,
"debit_in_account_currency": self.purchase_receipt_amount "debit_in_account_currency": self.purchase_receipt_amount,
"cost_center": self.cost_center
})) }))
if gl_entries: if gl_entries:

View File

@ -7,7 +7,7 @@
"doctype": "Report", "doctype": "Report",
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2019-03-29 17:18:06.678728", "modified": "2019-07-21 23:24:21.094269",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Procurement Tracker", "name": "Procurement Tracker",
@ -16,5 +16,12 @@
"ref_doctype": "Purchase Order", "ref_doctype": "Purchase Order",
"report_name": "Procurement Tracker", "report_name": "Procurement Tracker",
"report_type": "Script Report", "report_type": "Script Report",
"roles": [] "roles": [
{
"role": "Purchase Manager"
},
{
"role": "Purchase User"
}
]
} }

View File

@ -150,10 +150,10 @@ def get_conditions(filters):
"""% (filters.get('cost_center'), filters.get('project')) """% (filters.get('cost_center'), filters.get('project'))
if filters.get("from_date"): if filters.get("from_date"):
conditions += "AND transaction_date>=%s"% filters.get('from_date') conditions += " AND transaction_date>=%s"% filters.get('from_date')
if filters.get("to_date"): if filters.get("to_date"):
conditions += "AND transaction_date<=%s"% filters.get('to_date') conditions += " AND transaction_date<=%s"% filters.get('to_date')
return conditions return conditions
def get_data(filters): def get_data(filters):

View File

@ -157,6 +157,13 @@ def get_data():
"reference_doctype": "Purchase Order", "reference_doctype": "Purchase Order",
"onboard": 1, "onboard": 1,
}, },
{
"type": "report",
"is_query_report": True,
"name": "Procurement Tracker",
"reference_doctype": "Purchase Order",
"onboard": 1,
},
{ {
"type": "report", "type": "report",
"is_query_report": True, "is_query_report": True,
@ -228,5 +235,5 @@ def get_data():
} }
] ]
}, },
] ]

View File

@ -141,6 +141,11 @@ def get_data():
"name": "Campaign", "name": "Campaign",
"description": _("Sales campaigns."), "description": _("Sales campaigns."),
}, },
{
"type": "doctype",
"name": "Email Campaign",
"description": _("Sends Mails to lead or contact based on a Campaign schedule"),
},
{ {
"type": "doctype", "type": "doctype",
"name": "SMS Center", "name": "SMS Center",

View File

@ -219,6 +219,11 @@ def get_data():
"name": "Employee Onboarding", "name": "Employee Onboarding",
"dependencies": ["Job Applicant"], "dependencies": ["Job Applicant"],
}, },
{
"type": "doctype",
"name": "Employee Skill Map",
"dependencies": ["Employee"],
},
{ {
"type": "doctype", "type": "doctype",
"name": "Employee Promotion", "name": "Employee Promotion",

View File

@ -861,7 +861,7 @@ class AccountsController(TransactionBase):
if self.doctype in ("Sales Invoice", "Purchase Invoice"): if self.doctype in ("Sales Invoice", "Purchase Invoice"):
grand_total = grand_total - flt(self.write_off_amount) grand_total = grand_total - flt(self.write_off_amount)
if total != grand_total: if total != flt(grand_total, self.precision("grand_total")):
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total")) frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
def is_rounded_total_disabled(self): def is_rounded_total_disabled(self):

View File

@ -636,7 +636,8 @@ class BuyingController(StockController):
asset.set_missing_values() asset.set_missing_values()
asset.insert() asset.insert()
frappe.msgprint(_("Asset {0} created").format(asset.name)) asset_link = frappe.utils.get_link_to_form('Asset', asset.name)
frappe.msgprint(_("Asset {0} created").format(asset_link))
return asset.name return asset.name
def make_asset_movement(self, row): def make_asset_movement(self, row):

View File

@ -207,10 +207,10 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
idx desc, name idx desc, name
limit %(start)s, %(page_len)s """.format( limit %(start)s, %(page_len)s """.format(
fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'),
mcond=get_match_cond(doctype), mcond=get_match_cond(doctype).replace('%', '%%'),
key=frappe.db.escape(searchfield)), key=searchfield),
{ {
'txt': "%"+frappe.db.escape(txt)+"%", 'txt': '%' + txt + '%',
'_txt': txt.replace("%", ""), '_txt': txt.replace("%", ""),
'start': start or 0, 'start': start or 0,
'page_len': page_len or 20 'page_len': page_len or 20

View File

@ -40,7 +40,6 @@ status_map = {
["To Bill", "eval:self.per_delivered == 100 and self.per_billed < 100 and self.docstatus == 1"], ["To Bill", "eval:self.per_delivered == 100 and self.per_billed < 100 and self.docstatus == 1"],
["To Deliver", "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1"], ["To Deliver", "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_delivered == 100 and self.per_billed == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_delivered == 100 and self.per_billed == 100 and self.docstatus == 1"],
["Completed", "eval:self.order_type == 'Maintenance' and self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"], ["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"], ["Closed", "eval:self.status=='Closed'"],
["On Hold", "eval:self.status=='On Hold'"], ["On Hold", "eval:self.status=='On Hold'"],
@ -90,7 +89,8 @@ status_map = {
["Transferred", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'"], ["Transferred", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'"],
["Issued", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'"], ["Issued", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'"],
["Received", "eval:self.status != 'Stopped' and self.per_received == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"], ["Received", "eval:self.status != 'Stopped' and self.per_received == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
["Partially Received", "eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"] ["Partially Received", "eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
["Manufactured", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'"]
], ],
"Bank Transaction": [ "Bank Transaction": [
["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"], ["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"],

View File

@ -323,7 +323,7 @@ class calculate_taxes_and_totals(object):
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"]) self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
if self.doc.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: if self.doc.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]:
self.doc.base_grand_total = flt(self.doc.grand_total * self.doc.conversion_rate) \ self.doc.base_grand_total = flt(self.doc.grand_total * self.doc.conversion_rate, self.doc.precision("base_grand_total")) \
if self.doc.total_taxes_and_charges else self.doc.base_net_total if self.doc.total_taxes_and_charges else self.doc.base_net_total
else: else:
self.doc.taxes_and_charges_added = self.doc.taxes_and_charges_deducted = 0.0 self.doc.taxes_and_charges_added = self.doc.taxes_and_charges_deducted = 0.0

View File

@ -0,0 +1,38 @@
{
"creation": "2019-06-30 15:56:20.306901",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"email_template",
"send_after_days"
],
"fields": [
{
"fieldname": "send_after_days",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Send After (days)",
"reqd": 1
},
{
"fieldname": "email_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Email Template",
"options": "Email Template",
"reqd": 1
}
],
"istable": 1,
"modified": "2019-07-12 11:46:43.184123",
"modified_by": "Administrator",
"module": "CRM",
"name": "Campaign Email Schedule",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CampaignEmailSchedule(Document):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Email Campaign', {
email_campaign_for: function(frm) {
frm.set_value('recipient', '');
}
});

View File

@ -0,0 +1,95 @@
{
"autoname": "format:MAIL-CAMP-{YYYY}-{#####}",
"creation": "2019-06-30 16:05:30.015615",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"campaign_name",
"email_campaign_for",
"recipient",
"sender",
"column_break_4",
"start_date",
"end_date",
"status"
],
"fields": [
{
"fieldname": "campaign_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Campaign",
"options": "Campaign",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nScheduled\nIn Progress\nCompleted\nUnsubscribed",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Start Date",
"reqd": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"read_only": 1
},
{
"default": "Lead",
"fieldname": "email_campaign_for",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Email Campaign For ",
"options": "\nLead\nContact"
},
{
"fieldname": "recipient",
"fieldtype": "Dynamic Link",
"label": "Recipient",
"options": "email_campaign_for",
"reqd": 1
},
{
"default": "__user",
"fieldname": "sender",
"fieldtype": "Link",
"label": "Sender",
"options": "User"
}
],
"modified": "2019-07-12 13:47:37.261213",
"modified_by": "Administrator",
"module": "CRM",
"name": "Email Campaign",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import getdate, add_days, today, nowdate, cstr
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
class EmailCampaign(Document):
def validate(self):
self.set_date()
#checking if email is set for lead. Not checking for contact as email is a mandatory field for contact.
if self.email_campaign_for == "Lead":
self.validate_lead()
self.validate_email_campaign_already_exists()
self.update_status()
def set_date(self):
if getdate(self.start_date) < getdate(today()):
frappe.throw(_("Start Date cannot be before the current date"))
#set the end date as start date + max(send after days) in campaign schedule
send_after_days = []
campaign = frappe.get_doc("Campaign", self.campaign_name)
for entry in campaign.get("campaign_schedules"):
send_after_days.append(entry.send_after_days)
try:
end_date = add_days(getdate(self.start_date), max(send_after_days))
except ValueError:
frappe.throw(_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name))
def validate_lead(self):
lead_email_id = frappe.db.get_value("Lead", self.recipient, 'email_id')
if not lead_email_id:
lead_name = frappe.db.get_value("Lead", self.recipient, 'lead_name')
frappe.throw(_("Please set an email id for the Lead {0}").format(lead_name))
def validate_email_campaign_already_exists(self):
email_campaign_exists = frappe.db.exists("Email Campaign", {
"campaign_name": self.campaign_name,
"recipient": self.recipient,
"status": ("in", ["In Progress", "Scheduled"])
})
if email_campaign_exists:
frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient))
def update_status(self):
start_date = getdate(self.start_date)
end_date = getdate(self.end_date)
today_date = getdate(today())
if start_date > today_date:
self.status = "Scheduled"
elif end_date >= today_date:
self.status = "In Progress"
elif end_date < today_date:
self.status = "Completed"
#called through hooks to send campaign mails to leads
def send_email_to_leads_or_contacts():
email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('not in', ['Unsubscribed', 'Completed', 'Scheduled']) })
for camp in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", camp.name)
campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name)
for entry in campaign.get("campaign_schedules"):
scheduled_date = add_days(email_campaign.get('start_date'), entry.get('send_after_days'))
if scheduled_date == getdate(today()):
send_mail(entry, email_campaign)
def send_mail(entry, email_campaign):
recipient = frappe.db.get_value(email_campaign.email_campaign_for, email_campaign.get("recipient"), 'email_id')
email_template = frappe.get_doc("Email Template", entry.get("email_template"))
sender = frappe.db.get_value("User", email_campaign.get("sender"), 'email')
# send mail and link communication to document
comm = make(
doctype = "Email Campaign",
name = email_campaign.name,
subject = email_template.get("subject"),
content = email_template.get("response"),
sender = sender,
recipients = recipient,
communication_medium = "Email",
sent_or_received = "Sent",
send_email = True,
email_template = email_template.name
)
return comm
#called from hooks on doc_event Email Unsubscribe
def unsubscribe_recipient(unsubscribe, method):
if unsubscribe.reference_doctype == 'Email Campaign':
frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed")
#called through hooks to update email campaign status daily
def set_email_campaign_status():
email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('!=', 'Unsubscribed')})
for entry in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", entry.name)
email_campaign.update_status()

View File

@ -0,0 +1,11 @@
frappe.listview_settings['Email Campaign'] = {
get_indicator: function(doc) {
var colors = {
"Unsubscribed": "red",
"Scheduled": "blue",
"In Progress": "orange",
"Completed": "green"
};
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}
};

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestEmailCampaign(unittest.TestCase):
pass

View File

@ -7,14 +7,8 @@ cur_frm.email_field = "email_id";
erpnext.LeadController = frappe.ui.form.Controller.extend({ erpnext.LeadController = frappe.ui.form.Controller.extend({
setup: function () { setup: function () {
this.frm.make_methods = { this.frm.make_methods = {
'Quotation': () => erpnext.utils.create_new_doc('Quotation', { 'Quotation': this.make_quotation,
'quotation_to': this.frm.doc.doctype, 'Opportunity': this.create_opportunity
'party_name': this.frm.doc.name
}),
'Opportunity': () => erpnext.utils.create_new_doc('Opportunity', {
'opportunity_from': this.frm.doc.doctype,
'party_name': this.frm.doc.name
})
} }
this.frm.fields_dict.customer.get_query = function (doc, cdt, cdn) { this.frm.fields_dict.customer.get_query = function (doc, cdt, cdn) {

View File

@ -296,7 +296,9 @@ class TallyMigration(Document):
else: else:
function = voucher_to_journal_entry function = voucher_to_journal_entry
try: try:
vouchers.append(function(voucher)) processed_voucher = function(voucher)
if processed_voucher:
vouchers.append(processed_voucher)
except: except:
self.log(voucher) self.log(voucher)
return vouchers return vouchers
@ -342,6 +344,10 @@ class TallyMigration(Document):
account_field = "credit_to" account_field = "credit_to"
account_name = encode_company_abbr(self.tally_creditors_account, self.erpnext_company) account_name = encode_company_abbr(self.tally_creditors_account, self.erpnext_company)
price_list_field = "buying_price_list" price_list_field = "buying_price_list"
else:
# Do not handle vouchers other than "Purchase", "Debit Note", "Sales" and "Credit Note"
# Do not handle Custom Vouchers either
return
invoice = { invoice = {
"doctype": doctype, "doctype": doctype,

View File

@ -233,6 +233,9 @@ doc_events = {
}, },
"Contact":{ "Contact":{
"on_trash": "erpnext.support.doctype.issue.issue.update_issue" "on_trash": "erpnext.support.doctype.issue.issue.update_issue"
},
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
} }
} }
@ -272,6 +275,8 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.send_project_status_email_to_users", "erpnext.projects.doctype.project.project.send_project_status_email_to_users",
"erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.quality_management.doctype.quality_review.quality_review.review",
"erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status",
"erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts",
"erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status"
], ],
"daily_long": [ "daily_long": [
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms"

View File

@ -4,4 +4,17 @@ from __future__ import unicode_literals
import frappe import frappe
test_records = frappe.get_test_records('Designation') # test_records = frappe.get_test_records('Designation')
def create_designation(**args):
args = frappe._dict(args)
if frappe.db.exists("Designation", args.designation_name or "_Test designation"):
return frappe.get_doc("Designation", args.designation_name or "_Test designation")
designation = frappe.get_doc({
"doctype": "Designation",
"designation_name": args.designation_name or "_Test designation",
"description": args.description or "_Test description"
})
designation.save()
return designation

View File

@ -1,516 +1,124 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"beta": 0,
"creation": "2017-10-17 08:21:50.489773", "creation": "2017-10-17 08:21:50.489773",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"naming_series",
"full_name",
"status",
"transporter",
"column_break_2",
"employee",
"cell_number",
"address",
"license_details",
"license_number",
"column_break_8",
"issuing_date",
"column_break_10",
"expiry_date",
"driving_license_categories",
"driving_license_category"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "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": "Series", "label": "Series",
"length": 0, "options": "HR-DRI-.YYYY.-"
"no_copy": 0,
"options": "HR-DRI-.YYYY.-",
"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": "full_name", "fieldname": "full_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Full Name", "label": "Full Name",
"length": 0, "reqd": 1
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"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": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Status", "label": "Status",
"length": 0,
"no_copy": 0,
"options": "Active\nSuspended\nLeft", "options": "Active\nSuspended\nLeft",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 1, "allow_in_quick_entry": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Applicable for external driver", "description": "Applicable for external driver",
"fieldname": "transporter", "fieldname": "transporter",
"fieldtype": "Link", "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": "Transporter", "label": "Transporter",
"length": 0, "options": "Supplier"
"no_copy": 0,
"options": "Supplier",
"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": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break", "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,
"fieldname": "employee", "fieldname": "employee",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Employee", "label": "Employee",
"length": 0, "options": "Employee"
"no_copy": 0,
"options": "Employee",
"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": "cell_number", "fieldname": "cell_number",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "Cellphone Number"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Cellphone Number",
"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": "license_details", "fieldname": "license_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "License Details"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "License Details",
"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": "license_number", "fieldname": "license_number",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "License Number"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "License Number",
"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": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break", "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,
"fieldname": "issuing_date", "fieldname": "issuing_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0, "label": "Issuing Date"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Issuing Date",
"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": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break", "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,
"fieldname": "expiry_date", "fieldname": "expiry_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0, "label": "Expiry Date"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Expiry Date",
"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": "driving_license_categories", "fieldname": "driving_license_categories",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Driving License Categories"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Driving License Categories",
"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": "driving_license_category", "fieldname": "driving_license_category",
"fieldtype": "Table", "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": "Driving License Category", "label": "Driving License Category",
"length": 0, "options": "Driving License Category"
"no_copy": 0, },
"options": "Driving License Category", {
"permlevel": 0, "fieldname": "address",
"precision": "", "fieldtype": "Link",
"print_hide": 0, "label": "Address",
"print_hide_if_no_value": 0, "options": "Address"
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 0, "modified": "2019-07-18 16:29:14.151380",
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-10-03 19:53:50.924391",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Driver", "name": "Driver",
@ -518,72 +126,44 @@
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Fleet Manager", "role": "Fleet Manager",
"set_user_permissions": 0, "share": 1
"share": 1,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR User", "role": "HR User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "full_name", "search_fields": "full_name",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "full_name", "title_field": "full_name",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@ -12,6 +12,7 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeUserDisabledError(frappe.ValidationError): pass
class EmployeeLeftValidationError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass

View File

@ -3,6 +3,7 @@
"doctype": "DocType", "doctype": "DocType",
"document_type": "Other", "document_type": "Other",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [ "field_order": [
"employee_settings", "employee_settings",
"retirement_age", "retirement_age",
@ -22,7 +23,9 @@
"leave_status_notification_template", "leave_status_notification_template",
"column_break_18", "column_break_18",
"leave_approver_mandatory_in_leave_application", "leave_approver_mandatory_in_leave_application",
"show_leaves_of_all_department_members_in_calendar" "show_leaves_of_all_department_members_in_calendar",
"hiring_settings",
"check_vacancies"
], ],
"fields": [ "fields": [
{ {
@ -44,18 +47,6 @@
"label": "Employee Records to be created by", "label": "Employee Records to be created by",
"options": "Naming Series\nEmployee Number\nFull Name" "options": "Naming Series\nEmployee Number\nFull Name"
}, },
{
"fieldname": "leave_approval_notification_template",
"fieldtype": "Link",
"label": "Leave Approval Notification Template",
"options": "Email Template"
},
{
"fieldname": "leave_status_notification_template",
"fieldtype": "Link",
"label": "Leave Status Notification Template",
"options": "Email Template"
},
{ {
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -67,12 +58,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Stop Birthday Reminders" "label": "Stop Birthday Reminders"
}, },
{
"default": "1",
"fieldname": "leave_approver_mandatory_in_leave_application",
"fieldtype": "Check",
"label": "Leave Approver Mandatory In Leave Application"
},
{ {
"default": "1", "default": "1",
"fieldname": "expense_approver_mandatory_in_expense_claim", "fieldname": "expense_approver_mandatory_in_expense_claim",
@ -91,6 +76,15 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Include holidays in Total no. of Working Days" "label": "Include holidays in Total no. of Working Days"
}, },
{
"fieldname": "max_working_hours_against_timesheet",
"fieldtype": "Float",
"label": "Max working hours against Timesheet"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{ {
"default": "1", "default": "1",
"description": "Emails salary slip to employee based on preferred email selected in Employee", "description": "Emails salary slip to employee based on preferred email selected in Employee",
@ -115,15 +109,33 @@
"label": "Password Policy" "label": "Password Policy"
}, },
{ {
"fieldname": "max_working_hours_against_timesheet", "collapsible": 1,
"fieldtype": "Float",
"label": "Max working hours against Timesheet"
},
{
"fieldname": "leave_settings", "fieldname": "leave_settings",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Leave Settings" "label": "Leave Settings"
}, },
{
"fieldname": "leave_approval_notification_template",
"fieldtype": "Link",
"label": "Leave Approval Notification Template",
"options": "Email Template"
},
{
"fieldname": "leave_status_notification_template",
"fieldtype": "Link",
"label": "Leave Status Notification Template",
"options": "Email Template"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"default": "1",
"fieldname": "leave_approver_mandatory_in_leave_application",
"fieldtype": "Check",
"label": "Leave Approver Mandatory In Leave Application"
},
{ {
"default": "0", "default": "0",
"fieldname": "show_leaves_of_all_department_members_in_calendar", "fieldname": "show_leaves_of_all_department_members_in_calendar",
@ -131,18 +143,22 @@
"label": "Show Leaves Of All Department Members In Calendar" "label": "Show Leaves Of All Department Members In Calendar"
}, },
{ {
"fieldname": "column_break_11", "collapsible": 1,
"fieldtype": "Column Break" "fieldname": "hiring_settings",
"fieldtype": "Section Break",
"label": "Hiring Settings"
}, },
{ {
"fieldname": "column_break_18", "default": "0",
"fieldtype": "Column Break" "fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"modified": "2019-05-31 16:18:50.245872", "modified": "2019-07-01 18:59:55.256878",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR Settings", "name": "HR Settings",
@ -158,5 +174,6 @@
"write": 1 "write": 1
} }
], ],
"sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC"
} }

View File

@ -39,7 +39,7 @@
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0, "remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0, "translatable": 0,
@ -71,7 +71,7 @@
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0, "remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0, "translatable": 0,
@ -96,7 +96,7 @@
"label": "Status", "label": "Status",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "Open\nReplied\nRejected\nHold", "options": "Open\nReplied\nRejected\nHold\nAccepted",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
@ -346,7 +346,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-08-21 16:15:43.552049", "modified": "2019-06-21 16:15:43.552049",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Job Applicant", "name": "Job Applicant",

View File

@ -10,3 +10,14 @@ import unittest
class TestJobApplicant(unittest.TestCase): class TestJobApplicant(unittest.TestCase):
pass pass
def create_job_applicant(**args):
args = frappe._dict(args)
job_applicant = frappe.get_doc({
"doctype": "Job Applicant",
"applicant_name": args.applicant_name or "_Test Applicant",
"email_id": args.email_id or "test_applicant@example.com",
"status": args.status or "Open"
})
job_applicant.save()
return job_applicant

View File

@ -5,12 +5,56 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe import _
from frappe.utils.data import get_link_to_form
class JobOffer(Document): class JobOffer(Document):
def onload(self): def onload(self):
employee = frappe.db.get_value("Employee", {"job_applicant": self.job_applicant}, "name") or "" employee = frappe.db.get_value("Employee", {"job_applicant": self.job_applicant}, "name") or ""
self.set_onload("employee", employee) self.set_onload("employee", employee)
def validate(self):
self.validate_vacancies()
def validate_vacancies(self):
staffing_plan = get_staffing_plan_detail(self.designation, self.company, self.offer_date)
check_vacancies = frappe.get_single("HR Settings").check_vacancies
if staffing_plan and check_vacancies:
vacancies = frappe.db.get_value("Staffing Plan Detail", filters={"name": staffing_plan.name}, fieldname=['vacancies'])
job_offers = len(self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date))
if vacancies - job_offers <= 0:
frappe.throw(_("There are no vacancies under staffing plan {0}").format(get_link_to_form("Staffing Plan", staffing_plan.parent)))
def on_change(self):
update_job_applicant(self.status, self.job_applicant)
def get_job_offer(self, from_date, to_date):
''' Returns job offer created during a time period '''
return frappe.get_all("Job Offer", filters={
"offer_date": ['between', (from_date, to_date)],
"designation": self.designation,
"company": self.company
}, fields=['name'])
def update_job_applicant(status, job_applicant):
if status in ("Accepted", "Rejected"):
frappe.set_value("Job Applicant", job_applicant, "status", status)
def get_staffing_plan_detail(designation, company, offer_date):
detail = frappe.db.sql("""
SELECT spd.name as name,
sp.from_date as from_date,
sp.to_date as to_date,
sp.name as parent
FROM `tabStaffing Plan Detail` spd, `tabStaffing Plan` sp
WHERE
sp.docstatus=1
AND spd.designation=%s
AND sp.company=%s
AND %s between sp.from_date and sp.to_date
""", (designation, company, offer_date), as_dict=1)
return detail[0] if detail else None
@frappe.whitelist() @frappe.whitelist()
def make_employee(source_name, target_doc=None): def make_employee(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
@ -23,4 +67,3 @@ def make_employee(source_name, target_doc=None):
}} }}
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doc return doc

View File

@ -4,8 +4,78 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import nowdate, add_days
from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
from erpnext.hr.doctype.designation.test_designation import create_designation
from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company
# test_records = frappe.get_test_records('Job Offer') # test_records = frappe.get_test_records('Job Offer')
class TestJobOffer(unittest.TestCase): class TestJobOffer(unittest.TestCase):
pass def test_job_offer_creation_against_vacancies(self):
create_staffing_plan(staffing_details=[{
"designation": "Designer",
"vacancies": 0,
"estimated_cost_per_position": 5000
}])
frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
job_applicant = create_job_applicant(email_id="test_job_offer@example.com")
job_offer = create_job_offer(job_applicant=job_applicant.name, designation="Researcher")
self.assertRaises(frappe.ValidationError, job_offer.submit)
# test creation of job offer when vacancies are not present
frappe.db.set_value("HR Settings", None, "check_vacancies", 0)
job_offer.submit()
self.assertTrue(frappe.db.exists("Job Offer", job_offer.name))
def test_job_applicant_update(self):
create_staffing_plan()
job_applicant = create_job_applicant(email_id="test_job_applicants@example.com")
job_offer = create_job_offer(job_applicant=job_applicant.name)
job_offer.submit()
job_applicant.reload()
self.assertEquals(job_applicant.status, "Accepted")
# status update after rejection
job_offer.status = "Rejected"
job_offer.submit()
job_applicant.reload()
self.assertEquals(job_applicant.status, "Rejected")
def create_job_offer(**args):
args = frappe._dict(args)
if not args.job_applicant:
job_applicant = create_job_applicant()
if not frappe.db.exists("Designation", args.designation):
designation = create_designation(designation_name=args.designation)
job_offer = frappe.get_doc({
"doctype": "Job Offer",
"job_applicant": args.job_applicant or job_applicant.name,
"offer_date": args.offer_date or nowdate(),
"designation": args.designation or "Researcher",
"status": args.status or "Accepted"
})
return job_offer
def create_staffing_plan(**args):
args = frappe._dict(args)
make_company()
frappe.db.set_value("Company", "_Test Company", "is_group", 1)
if frappe.db.exists("Staffing Plan", args.name or "Test"):
return
staffing_plan = frappe.get_doc({
"doctype": "Staffing Plan",
"name": args.name or "Test",
"from_date": args.from_date or nowdate(),
"to_date": args.to_date or add_days(nowdate(), 10),
"staffing_details": args.staffing_details or [{
"designation": "Researcher",
"vacancies": 1,
"estimated_cost_per_position": 50000
}]
})
staffing_plan.insert()
staffing_plan.submit()
return staffing_plan

View File

@ -30,11 +30,11 @@ class LoanApplication(Document):
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100) monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
if monthly_interest_rate: if monthly_interest_rate:
min_repayment_amount = self.loan_amount*monthly_interest_rate min_repayment_amount = self.loan_amount*monthly_interest_rate
if self.repayment_amount - min_repayment_amount < 0: if self.repayment_amount - min_repayment_amount <= 0:
frappe.throw(_("Repayment Amount must be greater than " \ frappe.throw(_("Repayment Amount must be greater than " \
+ str(flt(min_repayment_amount, 2)))) + str(flt(min_repayment_amount, 2))))
self.repayment_periods = math.ceil(math.log(self.repayment_amount) - self.repayment_periods = math.ceil((math.log(self.repayment_amount) -
math.log(self.repayment_amount - min_repayment_amount) /(math.log(1 + monthly_interest_rate))) math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate)))
else: else:
self.repayment_periods = self.loan_amount / self.repayment_amount self.repayment_periods = self.loan_amount / self.repayment_amount

View File

@ -119,6 +119,8 @@ class PayrollEntry(Document):
frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args) frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args)
else: else:
create_salary_slips_for_employees(emp_list, args, publish_progress=False) create_salary_slips_for_employees(emp_list, args, publish_progress=False)
# since this method is called via frm.call this doc needs to be updated manually
self.reload()
def get_sal_slip_list(self, ss_status, as_dict=False): def get_sal_slip_list(self, ss_status, as_dict=False):
""" """

View File

@ -5,7 +5,7 @@ frappe.ui.form.on('Staffing Plan', {
setup: function(frm) { setup: function(frm) {
frm.set_query("designation", "staffing_details", function() { frm.set_query("designation", "staffing_details", function() {
let designations = []; let designations = [];
$.each(frm.doc.staffing_details, function(index, staff_detail) { (frm.doc.staffing_details || []).forEach(function(staff_detail) {
if(staff_detail.designation){ if(staff_detail.designation){
designations.push(staff_detail.designation) designations.push(staff_detail.designation)
} }
@ -25,69 +25,63 @@ frappe.ui.form.on('Staffing Plan', {
} }
}; };
}); });
} },
}); });
frappe.ui.form.on('Staffing Plan Detail', { frappe.ui.form.on('Staffing Plan Detail', {
designation: function(frm, cdt, cdn) { designation: function(frm, cdt, cdn) {
let child = locals[cdt][cdn] let child = locals[cdt][cdn];
if(frm.doc.company && child.designation){ if(frm.doc.company && child.designation) {
frappe.call({ set_number_of_positions(frm, cdt, cdn);
"method": "erpnext.hr.doctype.staffing_plan.staffing_plan.get_designation_counts",
args: {
designation: child.designation,
company: frm.doc.company
},
callback: function (data) {
if(data.message){
frappe.model.set_value(cdt, cdn, 'current_count', data.message.employee_count);
frappe.model.set_value(cdt, cdn, 'current_openings', data.message.job_openings);
if (child.number_of_positions < (data.message.employee_count + data.message.job_openings)){
frappe.model.set_value(cdt, cdn, 'number_of_positions', data.message.employee_count + data.message.job_openings);
}
}
else{ // No employees for this designation
frappe.model.set_value(cdt, cdn, 'current_count', 0);
frappe.model.set_value(cdt, cdn, 'current_openings', 0);
}
}
});
} }
}, },
number_of_positions: function(frm, cdt, cdn) { vacancies: function(frm, cdt, cdn) {
set_vacancies(frm, cdt, cdn); let child = locals[cdt][cdn];
if(child.vacancies < child.current_openings) {
frappe.throw(__("Vacancies cannot be lower than the current openings"));
}
set_number_of_positions(frm, cdt, cdn);
}, },
current_count: function(frm, cdt, cdn) { current_count: function(frm, cdt, cdn) {
set_vacancies(frm, cdt, cdn); set_number_of_positions(frm, cdt, cdn);
}, },
estimated_cost_per_position: function(frm, cdt, cdn) { estimated_cost_per_position: function(frm, cdt, cdn) {
let child = locals[cdt][cdn];
set_total_estimated_cost(frm, cdt, cdn); set_total_estimated_cost(frm, cdt, cdn);
} }
}); });
var set_vacancies = function(frm, cdt, cdn) { var set_number_of_positions = function(frm, cdt, cdn) {
let child = locals[cdt][cdn] let child = locals[cdt][cdn];
if (child.number_of_positions < (child.current_count + child.current_openings)){ if (!child.designation) frappe.throw(__("Please enter the designation"));
frappe.throw(__("Number of positions cannot be less then current count of employees")) frappe.call({
} "method": "erpnext.hr.doctype.staffing_plan.staffing_plan.get_designation_counts",
args: {
if(child.number_of_positions > 0) { designation: child.designation,
frappe.model.set_value(cdt, cdn, 'vacancies', child.number_of_positions - (child.current_count + child.current_openings)); company: frm.doc.company
} },
else{ callback: function (data) {
frappe.model.set_value(cdt, cdn, 'vacancies', 0); if(data.message){
} frappe.model.set_value(cdt, cdn, 'current_count', data.message.employee_count);
frappe.model.set_value(cdt, cdn, 'current_openings', data.message.job_openings);
let total_positions = cint(data.message.employee_count) + cint(child.vacancies);
if (cint(child.number_of_positions) < total_positions){
frappe.model.set_value(cdt, cdn, 'number_of_positions', total_positions);
}
}
else{ // No employees for this designation
frappe.model.set_value(cdt, cdn, 'current_count', 0);
frappe.model.set_value(cdt, cdn, 'current_openings', 0);
}
}
});
refresh_field("staffing_details");
set_total_estimated_cost(frm, cdt, cdn); set_total_estimated_cost(frm, cdt, cdn);
} }
// Note: Estimated Cost is calculated on number of Vacancies // Note: Estimated Cost is calculated on number of Vacancies
// Validate for > 0 ?
var set_total_estimated_cost = function(frm, cdt, cdn) { var set_total_estimated_cost = function(frm, cdt, cdn) {
let child = locals[cdt][cdn] let child = locals[cdt][cdn]
if(child.vacancies > 0 && child.estimated_cost_per_position) { if(child.vacancies > 0 && child.estimated_cost_per_position) {
@ -102,11 +96,11 @@ var set_total_estimated_cost = function(frm, cdt, cdn) {
var set_total_estimated_budget = function(frm) { var set_total_estimated_budget = function(frm) {
let estimated_budget = 0.0 let estimated_budget = 0.0
if(frm.doc.staffing_details) { if(frm.doc.staffing_details) {
$.each(frm.doc.staffing_details, function(index, staff_detail) { (frm.doc.staffing_details || []).forEach(function(staff_detail) {
if(staff_detail.total_estimated_cost){ if(staff_detail.total_estimated_cost){
estimated_budget += staff_detail.total_estimated_cost estimated_budget += staff_detail.total_estimated_cost
} }
}) })
frm.set_value('total_estimated_budget', estimated_budget); frm.set_value('total_estimated_budget', estimated_budget);
} }
} }

View File

@ -13,41 +13,39 @@ class ParentCompanyError(frappe.ValidationError): pass
class StaffingPlan(Document): class StaffingPlan(Document):
def validate(self): def validate(self):
self.validate_period()
self.validate_details()
self.set_total_estimated_budget()
def validate_period(self):
# Validate Dates # Validate Dates
if self.from_date and self.to_date and self.from_date > self.to_date: if self.from_date and self.to_date and self.from_date > self.to_date:
frappe.throw(_("From Date cannot be greater than To Date")) frappe.throw(_("From Date cannot be greater than To Date"))
self.total_estimated_budget = 0 def validate_details(self):
for detail in self.get("staffing_details"): for detail in self.get("staffing_details"):
self.set_vacancies(detail)
self.validate_overlap(detail) self.validate_overlap(detail)
self.validate_with_subsidiary_plans(detail) self.validate_with_subsidiary_plans(detail)
self.validate_with_parent_plan(detail) self.validate_with_parent_plan(detail)
def set_total_estimated_budget(self):
self.total_estimated_budget = 0
for detail in self.get("staffing_details"):
#Set readonly fields #Set readonly fields
self.set_number_of_positions(detail)
designation_counts = get_designation_counts(detail.designation, self.company) designation_counts = get_designation_counts(detail.designation, self.company)
detail.current_count = designation_counts['employee_count'] detail.current_count = designation_counts['employee_count']
detail.current_openings = designation_counts['job_openings'] detail.current_openings = designation_counts['job_openings']
if detail.number_of_positions < (detail.current_count + detail.current_openings): if detail.number_of_positions > 0:
frappe.throw(_("Number of positions cannot be less then current count of employees"))
elif detail.number_of_positions > 0:
detail.vacancies = detail.number_of_positions - (detail.current_count + detail.current_openings)
if detail.vacancies > 0 and detail.estimated_cost_per_position: if detail.vacancies > 0 and detail.estimated_cost_per_position:
detail.total_estimated_cost = detail.vacancies * detail.estimated_cost_per_position detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
else: detail.total_estimated_cost = 0
else: detail.vacancies = detail.number_of_positions = detail.total_estimated_cost = 0
self.total_estimated_budget += detail.total_estimated_cost self.total_estimated_budget += detail.total_estimated_cost
def set_vacancies(self, row): def set_number_of_positions(self, detail):
if not row.vacancies: detail.number_of_positions = cint(detail.vacancies) + cint(detail.current_count)
current_openings = 0
for field in ['current_count', 'current_openings']:
if row.get(field):
current_openings += row.get(field)
row.vacancies = row.number_of_positions - current_openings
def validate_overlap(self, staffing_plan_detail): def validate_overlap(self, staffing_plan_detail):
# Validate if any submitted Staffing Plan exist for any Designations in this plan # Validate if any submitted Staffing Plan exist for any Designations in this plan
@ -132,19 +130,24 @@ def get_designation_counts(designation, company):
if not designation: if not designation:
return False return False
employee_counts_dict = {} employee_counts = {}
lft, rgt = frappe.get_cached_value('Company', company, ["lft", "rgt"]) company_set = get_company_set(company)
employee_counts_dict["employee_count"] = frappe.db.sql("""select count(*) from `tabEmployee`
where designation = %s and status='Active'
and company in (select name from tabCompany where lft>=%s and rgt<=%s)
""", (designation, lft, rgt))[0][0]
employee_counts_dict['job_openings'] = frappe.db.sql("""select count(*) from `tabJob Opening` \ employee_counts["employee_count"] = frappe.db.get_value("Employee",
where designation=%s and status='Open' filters={
and company in (select name from tabCompany where lft>=%s and rgt<=%s) 'designation': designation,
""", (designation, lft, rgt))[0][0] 'status': 'Active',
'company': ('in', company_set)
}, fieldname=['count(name)'])
return employee_counts_dict employee_counts['job_openings'] = frappe.db.get_value("Job Opening",
filters={
'designation': designation,
'status': 'Open',
'company': ('in', company_set)
}, fieldname=['count(name)'])
return employee_counts
@frappe.whitelist() @frappe.whitelist()
def get_active_staffing_plan_details(company, designation, from_date=getdate(nowdate()), to_date=getdate(nowdate())): def get_active_staffing_plan_details(company, designation, from_date=getdate(nowdate()), to_date=getdate(nowdate())):
@ -165,3 +168,13 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
# Only a single staffing plan can be active for a designation on given date # Only a single staffing plan can be active for a designation on given date
return staffing_plan if staffing_plan else None return staffing_plan if staffing_plan else None
def get_company_set(company):
return frappe.db.sql_list("""
SELECT
name
FROM `tabCompany`
WHERE
parent_company=%(company)s
OR name=%(company)s
""", (dict(company=company)))

View File

@ -24,7 +24,7 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.to_date = add_days(nowdate(), 10) staffing_plan.to_date = add_days(nowdate(), 10)
staffing_plan.append("staffing_details", { staffing_plan.append("staffing_details", {
"designation": "Designer", "designation": "Designer",
"number_of_positions": 6, "vacancies": 6,
"estimated_cost_per_position": 50000 "estimated_cost_per_position": 50000
}) })
staffing_plan.insert() staffing_plan.insert()
@ -42,7 +42,7 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.to_date = add_days(nowdate(), 10) staffing_plan.to_date = add_days(nowdate(), 10)
staffing_plan.append("staffing_details", { staffing_plan.append("staffing_details", {
"designation": "Designer", "designation": "Designer",
"number_of_positions": 3, "vacancies": 3,
"estimated_cost_per_position": 45000 "estimated_cost_per_position": 45000
}) })
self.assertRaises(SubsidiaryCompanyError, staffing_plan.insert) self.assertRaises(SubsidiaryCompanyError, staffing_plan.insert)
@ -58,7 +58,7 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.to_date = add_days(nowdate(), 10) staffing_plan.to_date = add_days(nowdate(), 10)
staffing_plan.append("staffing_details", { staffing_plan.append("staffing_details", {
"designation": "Designer", "designation": "Designer",
"number_of_positions": 7, "vacancies": 7,
"estimated_cost_per_position": 50000 "estimated_cost_per_position": 50000
}) })
staffing_plan.insert() staffing_plan.insert()
@ -73,7 +73,7 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.to_date = add_days(nowdate(), 10) staffing_plan.to_date = add_days(nowdate(), 10)
staffing_plan.append("staffing_details", { staffing_plan.append("staffing_details", {
"designation": "Designer", "designation": "Designer",
"number_of_positions": 7, "vacancies": 7,
"estimated_cost_per_position": 60000 "estimated_cost_per_position": 60000
}) })
staffing_plan.insert() staffing_plan.insert()
@ -93,4 +93,4 @@ def make_company():
company.parent_company = "_Test Company" company.parent_company = "_Test Company"
company.default_currency = "INR" company.default_currency = "INR"
company.country = "India" company.country = "India"
company.insert() company.insert()

View File

@ -1,297 +1,79 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-04-13 18:04:20.978931", "creation": "2018-04-13 18:04:20.978931",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"designation",
"vacancies",
"estimated_cost_per_position",
"total_estimated_cost",
"column_break_5",
"current_count",
"current_openings",
"number_of_positions"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "designation", "fieldname": "designation",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Designation", "label": "Designation",
"length": 0,
"no_copy": 0,
"options": "Designation", "options": "Designation",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"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": "number_of_positions", "fieldname": "number_of_positions",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Number Of Positions", "label": "Number Of Positions",
"length": 0, "read_only": 1
"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": "estimated_cost_per_position", "fieldname": "estimated_cost_per_position",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Estimated Cost Per Position"
"label": "Estimated Cost Per Position",
"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": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break", "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,
"fieldname": "current_count", "fieldname": "current_count",
"fieldtype": "Int", "fieldtype": "Int",
"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": "Current Count", "label": "Current Count",
"length": 0, "read_only": 1
"no_copy": 0,
"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,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "current_openings", "fieldname": "current_openings",
"fieldtype": "Int", "fieldtype": "Int",
"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": "Current Openings", "label": "Current Openings",
"length": 0, "read_only": 1
"no_copy": 0,
"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,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "vacancies", "fieldname": "vacancies",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Vacancies"
"label": "Vacancies", },
"length": 0, {
"no_copy": 0, "fieldname": "total_estimated_cost",
"permlevel": 0, "fieldtype": "Currency",
"precision": "", "in_list_view": 1,
"print_hide": 0, "label": "Total Estimated Cost",
"print_hide_if_no_value": 0, "read_only": 1
"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": "total_estimated_cost",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Total Estimated Cost",
"length": 0,
"no_copy": 0,
"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
}
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "modified": "2019-06-24 18:40:37.140178",
"modified": "2018-06-01 17:03:38.020993",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Staffing Plan Detail", "name": "Staffing Plan Detail",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0 }
}

View File

@ -73,7 +73,7 @@ def create_columns():
def get_record(): def get_record():
data = [] data = []
loans = frappe.get_all("Loan", loans = frappe.get_all("Loan",
filters=[("status", "=", "Fully Disbursed")], filters=[("status", "=", "Disbursed")],
fields=["applicant", "applicant_name", "name", "loan_amount", "rate_of_interest", fields=["applicant", "applicant_name", "name", "loan_amount", "rate_of_interest",
"total_payment", "monthly_repayment_amount", "total_amount_paid"] "total_payment", "monthly_repayment_amount", "total_amount_paid"]
) )

View File

@ -578,6 +578,8 @@ class BOM(WebsiteGenerator):
for d in self.operations: for d in self.operations:
if not d.description: if not d.description:
d.description = frappe.db.get_value('Operation', d.operation, 'description') d.description = frappe.db.get_value('Operation', d.operation, 'description')
if not d.batch_size > 0:
d.batch_size = 1
def get_list_context(context): def get_list_context(context):
context.title = _("Bill of Materials") context.title = _("Bill of Materials")

View File

@ -1,361 +1,119 @@
{ {
"allow_copy": 0, "creation": "2013-02-22 01:27:49",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "document_type": "Setup",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2013-02-22 01:27:49", "field_order": [
"custom": 0, "operation",
"docstatus": 0, "workstation",
"doctype": "DocType", "description",
"document_type": "Setup", "col_break1",
"editable_grid": 1, "hour_rate",
"engine": "InnoDB", "time_in_mins",
"batch_size",
"operating_cost",
"base_hour_rate",
"base_operating_cost",
"image"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "operation",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Operation",
"columns": 0, "oldfieldname": "operation_no",
"fieldname": "operation", "oldfieldtype": "Data",
"fieldtype": "Link", "options": "Operation",
"hidden": 0, "reqd": 1
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Operation",
"length": 0,
"no_copy": 0,
"oldfieldname": "operation_no",
"oldfieldtype": "Data",
"options": "Operation",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "workstation",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Workstation",
"columns": 0, "oldfieldname": "workstation",
"fieldname": "workstation", "oldfieldtype": "Link",
"fieldtype": "Link", "options": "Workstation"
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Workstation",
"length": 0,
"no_copy": 0,
"oldfieldname": "workstation",
"oldfieldtype": "Link",
"options": "Workstation",
"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, "fieldname": "description",
"allow_on_submit": 0, "fieldtype": "Text Editor",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Description",
"columns": 0, "oldfieldname": "opn_description",
"fieldname": "description", "oldfieldtype": "Text"
"fieldtype": "Text Editor", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "opn_description",
"oldfieldtype": "Text",
"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, "fieldname": "col_break1",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "col_break1",
"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,
"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, "fieldname": "hour_rate",
"allow_on_submit": 0, "fieldtype": "Currency",
"bold": 0, "label": "Hour Rate",
"collapsible": 0, "oldfieldname": "hour_rate",
"columns": 0, "oldfieldtype": "Currency",
"fieldname": "hour_rate", "options": "currency"
"fieldtype": "Currency", },
"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": "Hour Rate",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate",
"oldfieldtype": "Currency",
"options": "currency",
"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, "description": "In minutes",
"allow_on_submit": 0, "fieldname": "time_in_mins",
"bold": 0, "fieldtype": "Float",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "Operation Time ",
"description": "In minutes", "oldfieldname": "time_in_mins",
"fieldname": "time_in_mins", "oldfieldtype": "Currency",
"fieldtype": "Float", "reqd": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Operation Time ",
"length": 0,
"no_copy": 0,
"oldfieldname": "time_in_mins",
"oldfieldtype": "Currency",
"options": "",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "operating_cost",
"allow_on_submit": 0, "fieldtype": "Currency",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Operating Cost",
"columns": 0, "oldfieldname": "operating_cost",
"fieldname": "operating_cost", "oldfieldtype": "Currency",
"fieldtype": "Currency", "options": "currency",
"hidden": 0, "read_only": 1
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Operating Cost",
"length": 0,
"no_copy": 0,
"oldfieldname": "operating_cost",
"oldfieldtype": "Currency",
"options": "currency",
"permlevel": 0,
"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, "fieldname": "base_hour_rate",
"allow_on_submit": 0, "fieldtype": "Currency",
"bold": 0, "label": "Base Hour Rate(Company Currency)",
"collapsible": 0, "options": "Company:company:default_currency",
"columns": 0, "print_hide": 1,
"fieldname": "base_hour_rate", "read_only": 1
"fieldtype": "Currency", },
"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": "Base Hour Rate(Company Currency)",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"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, "default": "5",
"allow_on_submit": 0, "fieldname": "base_operating_cost",
"bold": 0, "fieldtype": "Currency",
"collapsible": 0, "label": "Operating Cost(Company Currency)",
"columns": 0, "options": "Company:company:default_currency",
"default": "5", "print_hide": 1,
"fieldname": "base_operating_cost", "read_only": 1
"fieldtype": "Currency", },
"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": "Operating Cost(Company Currency)",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"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, "fieldname": "image",
"allow_on_submit": 0, "fieldtype": "Attach",
"bold": 0, "label": "Image"
"collapsible": 0, },
"columns": 0, {
"fieldname": "image", "default": "1",
"fieldtype": "Attach", "fieldname": "batch_size",
"hidden": 0, "fieldtype": "Int",
"ignore_user_permissions": 0, "label": "Batch Size"
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Image",
"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
} }
], ],
"has_web_view": 0, "idx": 1,
"hide_heading": 0, "istable": 1,
"hide_toolbar": 0, "modified": "2019-07-16 22:35:55.374037",
"idx": 1, "modified_by": "govindsmenokee@gmail.com",
"image_view": 0, "module": "Manufacturing",
"in_create": 0, "name": "BOM Operation",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "sort_field": "modified",
"max_attachments": 0, "sort_order": "DESC"
"modified": "2018-03-26 09:55:28.107451", }
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 0,
"track_seen": 0
}

View File

@ -505,7 +505,7 @@ def get_material_request_items(row, sales_order,
total_qty = row['qty'] total_qty = row['qty']
required_qty = 0 required_qty = 0
if ignore_existing_ordered_qty or bin_dict.get("projected_qty") < 0: if ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
required_qty = total_qty required_qty = total_qty
elif total_qty > bin_dict.get("projected_qty"): elif total_qty > bin_dict.get("projected_qty"):
required_qty = total_qty - bin_dict.get("projected_qty") required_qty = total_qty - bin_dict.get("projected_qty")

View File

@ -4,6 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import json import json
import math
from frappe import _ from frappe import _
from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate
from frappe.model.document import Document from frappe.model.document import Document
@ -323,7 +324,7 @@ class WorkOrder(Document):
select select
operation, description, workstation, idx, operation, description, workstation, idx,
base_hour_rate as hour_rate, time_in_mins, base_hour_rate as hour_rate, time_in_mins,
"Pending" as status, parent as bom "Pending" as status, parent as bom, batch_size
from from
`tabBOM Operation` `tabBOM Operation`
where where
@ -348,7 +349,7 @@ class WorkOrder(Document):
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
for d in self.get("operations"): for d in self.get("operations"):
d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * flt(self.qty) d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * math.ceil(flt(self.qty) / flt(d.batch_size))
self.calculate_operating_cost() self.calculate_operating_cost()

View File

@ -1,690 +1,200 @@
{ {
"allow_copy": 0, "creation": "2014-10-16 14:35:41.950175",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "editable_grid": 1,
"allow_rename": 0, "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2014-10-16 14:35:41.950175", "details",
"custom": 0, "operation",
"docstatus": 0, "bom",
"doctype": "DocType", "description",
"document_type": "", "col_break1",
"editable_grid": 1, "completed_qty",
"engine": "InnoDB", "status",
"workstation",
"estimated_time_and_cost",
"planned_start_time",
"planned_end_time",
"column_break_10",
"time_in_mins",
"hour_rate",
"batch_size",
"planned_operating_cost",
"section_break_9",
"actual_start_time",
"actual_end_time",
"column_break_11",
"actual_operation_time",
"actual_operating_cost"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "details",
"allow_on_submit": 0, "fieldtype": "Section Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "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": "",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "operation",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Operation",
"columns": 0, "oldfieldname": "operation_no",
"fieldname": "operation", "oldfieldtype": "Data",
"fieldtype": "Link", "options": "Operation",
"hidden": 0, "read_only": 1,
"ignore_user_permissions": 0, "reqd": 1
"ignore_xss_filter": 0, },
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Operation",
"length": 0,
"no_copy": 0,
"oldfieldname": "operation_no",
"oldfieldtype": "Data",
"options": "Operation",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "bom",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "BOM",
"collapsible": 0, "no_copy": 1,
"columns": 0, "options": "BOM",
"fieldname": "bom", "print_hide": 1,
"fieldtype": "Link", "read_only": 1
"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": "BOM",
"length": 0,
"no_copy": 1,
"options": "BOM",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "description",
"allow_on_submit": 0, "fieldtype": "Text Editor",
"bold": 0, "label": "Operation Description",
"collapsible": 0, "oldfieldname": "opn_description",
"columns": 0, "oldfieldtype": "Text",
"fieldname": "description", "read_only": 1
"fieldtype": "Text Editor", },
"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": "Operation Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "opn_description",
"oldfieldtype": "Text",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "col_break1",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "col_break1",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "description": "Operation completed for how many finished goods?",
"allow_on_submit": 0, "fieldname": "completed_qty",
"bold": 0, "fieldtype": "Float",
"collapsible": 0, "label": "Completed Qty",
"columns": 0, "no_copy": 1,
"description": "Operation completed for how many finished goods?", "read_only": 1
"fieldname": "completed_qty", },
"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": "Completed Qty",
"length": 0,
"no_copy": 1,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "Pending",
"allow_on_submit": 0, "fieldname": "status",
"bold": 0, "fieldtype": "Select",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "Status",
"default": "Pending", "no_copy": 1,
"fieldname": "status", "options": "Pending\nWork in Progress\nCompleted",
"fieldtype": "Select", "read_only": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Status",
"length": 0,
"no_copy": 1,
"options": "Pending\nWork in Progress\nCompleted",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "workstation",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Workstation",
"columns": 0, "oldfieldname": "workstation",
"fieldname": "workstation", "oldfieldtype": "Link",
"fieldtype": "Link", "options": "Workstation"
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Workstation",
"length": 0,
"no_copy": 0,
"oldfieldname": "workstation",
"oldfieldtype": "Link",
"options": "Workstation",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "estimated_time_and_cost",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Estimated Time and Cost"
"collapsible": 0, },
"columns": 0,
"fieldname": "estimated_time_and_cost",
"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": "Estimated Time and Cost",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "planned_start_time",
"allow_on_submit": 0, "fieldtype": "Datetime",
"bold": 0, "label": "Planned Start Time",
"collapsible": 0, "no_copy": 1,
"columns": 0, "read_only": 1
"fieldname": "planned_start_time", },
"fieldtype": "Datetime",
"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": "Planned Start Time",
"length": 0,
"no_copy": 1,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "planned_end_time",
"allow_on_submit": 0, "fieldtype": "Datetime",
"bold": 0, "label": "Planned End Time",
"collapsible": 0, "no_copy": 1,
"columns": 0, "read_only": 1
"fieldname": "planned_end_time", },
"fieldtype": "Datetime",
"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": "Planned End Time",
"length": 0,
"no_copy": 1,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_10",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_10",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "description": "in Minutes",
"allow_on_submit": 0, "fieldname": "time_in_mins",
"bold": 0, "fieldtype": "Float",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "Operation Time",
"description": "in Minutes", "oldfieldname": "time_in_mins",
"fieldname": "time_in_mins", "oldfieldtype": "Currency",
"fieldtype": "Float", "reqd": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Operation Time",
"length": 0,
"no_copy": 0,
"oldfieldname": "time_in_mins",
"oldfieldtype": "Currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "hour_rate",
"allow_on_submit": 0, "fieldtype": "Float",
"bold": 0, "label": "Hour Rate",
"collapsible": 0, "oldfieldname": "hour_rate",
"columns": 0, "oldfieldtype": "Currency",
"fieldname": "hour_rate", "read_only": 1
"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": "Hour Rate",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate",
"oldfieldtype": "Currency",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "planned_operating_cost",
"allow_on_submit": 0, "fieldtype": "Currency",
"bold": 0, "label": "Planned Operating Cost",
"collapsible": 0, "options": "Company:company:default_currency",
"columns": 0, "read_only": 1
"fieldname": "planned_operating_cost", },
"fieldtype": "Currency",
"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": "Planned Operating Cost",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_9",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Actual Time and Cost"
"collapsible": 0, },
"columns": 0,
"fieldname": "section_break_9",
"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": "Actual Time and Cost",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "actual_start_time",
"allow_on_submit": 0, "fieldtype": "Datetime",
"bold": 0, "label": "Actual Start Time",
"collapsible": 0, "no_copy": 1,
"columns": 0, "read_only": 1
"fieldname": "actual_start_time", },
"fieldtype": "Datetime",
"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": "Actual Start Time",
"length": 0,
"no_copy": 1,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "description": "Updated via 'Time Log'",
"allow_on_submit": 0, "fieldname": "actual_end_time",
"bold": 0, "fieldtype": "Datetime",
"collapsible": 0, "label": "Actual End Time",
"columns": 0, "no_copy": 1,
"description": "Updated via 'Time Log'", "read_only": 1
"fieldname": "actual_end_time", },
"fieldtype": "Datetime",
"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": "Actual End Time",
"length": 0,
"no_copy": 1,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_11",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_11",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "description": "in Minutes\nUpdated via 'Time Log'",
"allow_on_submit": 0, "fieldname": "actual_operation_time",
"bold": 0, "fieldtype": "Float",
"collapsible": 0, "label": "Actual Operation Time",
"columns": 0, "no_copy": 1,
"description": "in Minutes\nUpdated via 'Time Log'", "read_only": 1
"fieldname": "actual_operation_time", },
"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": "Actual Operation Time",
"length": 0,
"no_copy": 1,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "description": "(Hour Rate / 60) * Actual Operation Time",
"allow_on_submit": 0, "fieldname": "actual_operating_cost",
"bold": 0, "fieldtype": "Currency",
"collapsible": 0, "label": "Actual Operating Cost",
"columns": 0, "no_copy": 1,
"description": "(Hour Rate / 60) * Actual Operation Time", "options": "Company:company:default_currency",
"fieldname": "actual_operating_cost", "read_only": 1
"fieldtype": "Currency", },
"hidden": 0, {
"ignore_user_permissions": 0, "fieldname": "batch_size",
"ignore_xss_filter": 0, "fieldtype": "Int",
"in_filter": 0, "label": "Batch Size",
"in_global_search": 0, "read_only": 1
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Actual Operating Cost",
"length": 0,
"no_copy": 1,
"options": "Company:company:default_currency",
"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,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "modified": "2019-07-16 23:01:07.720337",
"hide_toolbar": 0, "modified_by": "govindsmenokee@gmail.com",
"idx": 0, "module": "Manufacturing",
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-02-13 02:58:11.328693",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",
"name_case": "", "owner": "Administrator",
"owner": "Administrator", "permissions": [],
"permissions": [], "sort_field": "modified",
"quick_entry": 0, "sort_order": "DESC",
"read_only": 0, "track_changes": 1
"read_only_onload": 0, }
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@ -618,8 +618,10 @@ erpnext.patches.v11_1.set_missing_opportunity_from
erpnext.patches.v12_0.set_quotation_status erpnext.patches.v12_0.set_quotation_status
erpnext.patches.v12_0.set_priority_for_support erpnext.patches.v12_0.set_priority_for_support
erpnext.patches.v12_0.delete_priority_property_setter erpnext.patches.v12_0.delete_priority_property_setter
erpnext.patches.v12_0.set_default_batch_size
execute:frappe.delete_doc("DocType", "Project Task") execute:frappe.delete_doc("DocType", "Project Task")
erpnext.patches.v11_1.update_default_supplier_in_item_defaults erpnext.patches.v11_1.update_default_supplier_in_item_defaults
erpnext.patches.v12_0.update_due_date_in_gle erpnext.patches.v12_0.update_due_date_in_gle
erpnext.patches.v12_0.add_default_buying_selling_terms_in_company erpnext.patches.v12_0.add_default_buying_selling_terms_in_company
erpnext.patches.v12_0.update_ewaybill_field_position erpnext.patches.v12_0.update_ewaybill_field_position
erpnext.patches.v11_1.set_status_for_material_request_type_manufacture

View File

@ -0,0 +1,9 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.db.sql("""
update `tabMaterial Request`
set status='Manufactured'
where docstatus=1 and material_request_type='Manufacture' and per_ordered=100 and status != 'Stopped'
""")

View File

@ -0,0 +1,19 @@
import frappe
def execute():
frappe.reload_doc("manufacturing", "doctype", "bom_operation")
frappe.reload_doc("manufacturing", "doctype", "work_order_operation")
frappe.db.sql("""
UPDATE
`tabBOM Operation` bo
SET
bo.batch_size = 1
""")
frappe.db.sql("""
UPDATE
`tabWork Order Operation` wop
SET
wop.batch_size = 1
""")

View File

@ -1,6 +1,23 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Project", { frappe.ui.form.on("Project", {
setup(frm) {
frm.make_methods = {
'Timesheet': () => {
let doctype = 'Timesheet';
frappe.model.with_doctype(doctype, () => {
let new_doc = frappe.model.get_new_doc(doctype);
// add a new row and set the project
let time_log = frappe.model.get_new_doc('Timesheet Detail');
time_log.project = frm.doc.name;
new_doc.time_logs = [time_log];
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
},
}
},
onload: function (frm) { onload: function (frm) {
var so = frappe.meta.get_docfield("Project", "sales_order"); var so = frappe.meta.get_docfield("Project", "sales_order");
so.get_route_options_for_new_doc = function (field) { so.get_route_options_for_new_doc = function (field) {

View File

@ -6,18 +6,13 @@
"description": "Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ", "description": "Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB",
"field_order": [ "field_order": [
"campaign", "campaign",
"campaign_name", "campaign_name",
"naming_series", "naming_series",
"from_date", "campaign_schedules_section",
"column_break1", "campaign_schedules",
"status",
"to_date",
"budget_section",
"currency",
"column_break2",
"budget",
"description_section", "description_section",
"description" "description"
], ],
@ -52,57 +47,25 @@
"oldfieldtype": "Text", "oldfieldtype": "Text",
"width": "300px" "width": "300px"
}, },
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "\nPlanned\nIn Progress\nCompleted\nCancelled",
"reqd": 1,
"default": "Planned"
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date"
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break"
},
{
"fieldname": "budget",
"fieldtype": "Currency",
"label": "Budget"
},
{ {
"fieldname": "description_section", "fieldname": "description_section",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"fieldname": "currency", "fieldname": "campaign_schedules",
"fieldtype": "Link", "fieldtype": "Table",
"label": "Currency", "label": "Campaign Schedules",
"options": "Currency" "options": "Campaign Email Schedule"
}, },
{ {
"fieldname": "column_break2", "fieldname": "campaign_schedules_section",
"fieldtype": "Column Break"
},
{
"fieldname": "budget_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "BUDGET" "label": "Campaign Schedules"
} }
], ],
"icon": "fa fa-bullhorn", "icon": "fa fa-bullhorn",
"idx": 1, "idx": 1,
"modified": "2019-04-29 22:09:39.251884", "modified": "2019-07-22 12:03:39.832342",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Campaign", "name": "Campaign",
@ -140,5 +103,7 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1 "quick_entry": 1,
} "sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,13 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'campaign_name',
'transactions': [
{
'label': _('Email Campaigns'),
'items': ['Email Campaign']
}
],
}

View File

@ -509,10 +509,12 @@ def make_material_request(source_name, target_doc=None):
doc.material_request_type = "Purchase" doc.material_request_type = "Purchase"
def update_item(source, target, source_parent): def update_item(source, target, source_parent):
# qty is for packed items, because packed items don't have stock_qty field
qty = source.get("stock_qty") or source.get("qty")
target.project = source_parent.project target.project = source_parent.project
target.qty = source.stock_qty - requested_item_qty.get(source.name, 0) target.qty = qty - requested_item_qty.get(source.name, 0)
target.conversion_factor = 1 target.conversion_factor = 1
target.stock_qty = source.stock_qty - requested_item_qty.get(source.name, 0) target.stock_qty = qty - requested_item_qty.get(source.name, 0)
doc = get_mapped_doc("Sales Order", source_name, { doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": { "Sales Order": {

View File

@ -69,8 +69,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
"items": get_product_list_for_group(product_group = self.name, start=start, "items": get_product_list_for_group(product_group = self.name, start=start,
limit=context.page_length + 1, search=frappe.form_dict.get("search")), limit=context.page_length + 1, search=frappe.form_dict.get("search")),
"parents": get_parent_item_groups(self.parent_item_group), "parents": get_parent_item_groups(self.parent_item_group),
"title": self.name, "title": self.name
"products_as_list": cint(frappe.db.get_single_value('Products Settings', 'products_as_list'))
}) })
if self.slideshow: if self.slideshow:

View File

@ -16,18 +16,47 @@ erpnext.stock.ItemDashboard = Class.extend({
this.content = $(frappe.render_template('item_dashboard')).appendTo(this.parent); this.content = $(frappe.render_template('item_dashboard')).appendTo(this.parent);
this.result = this.content.find('.result'); this.result = this.content.find('.result');
// move
this.content.on('click', '.btn-move', function() { this.content.on('click', '.btn-move', function() {
erpnext.stock.move_item(unescape($(this).attr('data-item')), $(this).attr('data-warehouse'), handle_move_add($(this), "Move")
null, $(this).attr('data-actual_qty'), null, function() { me.refresh(); });
}); });
this.content.on('click', '.btn-add', function() { this.content.on('click', '.btn-add', function() {
erpnext.stock.move_item(unescape($(this).attr('data-item')), null, $(this).attr('data-warehouse'), handle_move_add($(this), "Add")
$(this).attr('data-actual_qty'), $(this).attr('data-rate'),
function() { me.refresh(); });
}); });
function handle_move_add(element, action) {
let item = unescape(element.attr('data-item'));
let warehouse = unescape(element.attr('data-warehouse'));
let actual_qty = unescape(element.attr('data-actual_qty'));
let disable_quick_entry = Number(unescape(element.attr('data-disable_quick_entry')));
let entry_type = action === "Move" ? "Material Transfer": null;
if (disable_quick_entry) {
open_stock_entry(item, warehouse, entry_type);
} else {
if (action === "Add") {
let rate = unescape($(this).attr('data-rate'));
erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function() { me.refresh(); });
}
else {
erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function() { me.refresh(); });
}
}
}
function open_stock_entry(item, warehouse, entry_type) {
frappe.model.with_doctype('Stock Entry', function() {
var doc = frappe.model.get_new_doc('Stock Entry');
if (entry_type) doc.stock_entry_type = entry_type;
var row = frappe.model.add_child(doc, 'items');
row.item_code = item;
row.s_warehouse = warehouse;
frappe.set_route('Form', doc.doctype, doc.name);
})
}
// more // more
this.content.find('.btn-more').on('click', function() { this.content.find('.btn-more').on('click', function() {
me.start += 20; me.start += 20;
@ -196,4 +225,4 @@ erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callb
frappe.set_route('Form', doc.doctype, doc.name); frappe.set_route('Form', doc.doctype, doc.name);
}) })
}); });
} }

View File

@ -44,7 +44,9 @@ def get_data(item_code=None, warehouse=None, item_group=None,
for item in items: for item in items:
item.update({ item.update({
'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name') 'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'),
'disable_quick_entry': frappe.get_cached_value("Item", item.item_code, 'has_batch_no')
or frappe.get_cached_value("Item", item.item_code, 'has_serial_no'),
}) })
return items return items

View File

@ -43,11 +43,13 @@
<div class="col-sm-2 text-right" style="margin-top: 8px;"> <div class="col-sm-2 text-right" style="margin-top: 8px;">
{% if d.actual_qty %} {% if d.actual_qty %}
<button class="btn btn-default btn-xs btn-move" <button class="btn btn-default btn-xs btn-move"
data-disable_quick_entry="{{ d.disable_quick_entry }}"
data-warehouse="{{ d.warehouse }}" data-warehouse="{{ d.warehouse }}"
data-actual_qty="{{ d.actual_qty }}" data-actual_qty="{{ d.actual_qty }}"
data-item="{{ escape(d.item_code) }}">{{ __("Move") }}</a> data-item="{{ escape(d.item_code) }}">{{ __("Move") }}</a>
{% endif %} {% endif %}
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add" <button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add"
data-disable_quick_entry="{{ d.disable_quick_entry }}"
data-warehouse="{{ d.warehouse }}" data-warehouse="{{ d.warehouse }}"
data-actual_qty="{{ d.actual_qty }}" data-actual_qty="{{ d.actual_qty }}"
data-item="{{ escape(d.item_code) }}" data-item="{{ escape(d.item_code) }}"

View File

@ -122,8 +122,8 @@ class Batch(Document):
self.expiry_date = add_days(self.manufacturing_date, shelf_life_in_days) self.expiry_date = add_days(self.manufacturing_date, shelf_life_in_days)
if has_expiry_date and not self.expiry_date: if has_expiry_date and not self.expiry_date:
frappe.throw(_('Expiry date is mandatory for selected item')) frappe.msgprint(_('Expiry date is mandatory for selected item.'))
frappe.msgprint(_('Set items shelf life in days, to set expiry based on manufacturing_date plus self life')) frappe.throw(_("Set item's shelf life in days, to set expiry based on manufacturing date plus shelf-life."))
def get_name_from_naming_series(self): def get_name_from_naming_series(self):
""" """

View File

@ -67,43 +67,34 @@ frappe.ui.form.on('Delivery Trip', {
}, },
calculate_arrival_time: function (frm) { calculate_arrival_time: function (frm) {
frappe.db.get_value("Google Maps Settings", { name: "Google Maps Settings" }, "enabled", (r) => { if (!frm.doc.driver_address) {
if (r.enabled == 0) { frappe.throw(__("Cannot Calculate Arrival Time as Driver Address is Missing."));
frappe.throw(__("Please enable Google Maps Settings to estimate and optimize routes")); }
} else { frappe.show_alert({
frappe.call({ message: "Calculating Arrival Times",
method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.get_arrival_times', indicator: 'orange'
freeze: true, });
freeze_message: __("Updating estimated arrival times."), frm.call("process_route", {
args: { optimize: false,
delivery_trip: frm.doc.name, }, () => {
}, frm.reload_doc();
callback: function (r) { });
frm.reload_doc();
}
});
}
})
}, },
optimize_route: function (frm) { optimize_route: function (frm) {
frappe.db.get_value("Google Maps Settings", {name: "Google Maps Settings"}, "enabled", (r) => { if (!frm.doc.driver_address) {
if (r.enabled == 0) { frappe.throw(__("Cannot Optimize Route as Driver Address is Missing."));
frappe.throw(__("Please enable Google Maps Settings to estimate and optimize routes")); }
} else { frappe.show_alert({
frappe.call({ message: "Optimizing Route",
method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.optimize_route', indicator: 'orange'
freeze: true, });
freeze_message: __("Optimizing routes."), frm.call("process_route", {
args: { optimize: true,
delivery_trip: frm.doc.name, }, () => {
}, frm.reload_doc();
callback: function (r) { });
frm.reload_doc();
}
});
}
})
}, },
notify_customers: function (frm) { notify_customers: function (frm) {

View File

@ -1,735 +1,214 @@
{ {
"allow_copy": 0, "autoname": "naming_series:",
"allow_events_in_timeline": 0, "creation": "2017-10-16 16:45:48.293335",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "document_type": "Document",
"allow_rename": 0, "editable_grid": 1,
"autoname": "naming_series:", "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2017-10-16 16:45:48.293335", "naming_series",
"custom": 0, "company",
"docstatus": 0, "column_break_2",
"doctype": "DocType", "email_notification_sent",
"document_type": "Document", "section_break_3",
"editable_grid": 1, "driver",
"engine": "InnoDB", "driver_name",
"driver_address",
"total_distance",
"uom",
"column_break_4",
"vehicle",
"departure_time",
"delivery_service_stops",
"delivery_stops",
"calculate_arrival_time",
"optimize_route",
"section_break_15",
"status",
"cb_more_info",
"amended_from"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "naming_series",
"allow_in_quick_entry": 0, "fieldtype": "Select",
"allow_on_submit": 0, "label": "Series",
"bold": 0, "options": "MAT-DT-.YYYY.-"
"collapsible": 0, },
"columns": 0,
"default": "",
"fieldname": "naming_series",
"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": "Series",
"length": 0,
"no_copy": 0,
"options": "MAT-DT-.YYYY.-",
"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, "fieldname": "company",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Company",
"bold": 0, "options": "Company",
"collapsible": 0, "reqd": 1
"columns": 0, },
"fieldname": "company",
"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": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_2",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"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_on_submit": 1,
"allow_in_quick_entry": 0, "default": "0",
"allow_on_submit": 1, "fieldname": "email_notification_sent",
"bold": 0, "fieldtype": "Check",
"collapsible": 0, "label": "Initial Email Notification Sent",
"columns": 0, "read_only": 1
"fieldname": "email_notification_sent", },
"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": "Initial Email Notification Sent",
"length": 0,
"no_copy": 0,
"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, "fieldname": "section_break_3",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Delivery Details"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_3",
"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": "Delivery Details",
"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, "fieldname": "driver",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Driver",
"bold": 0, "options": "Driver",
"collapsible": 0, "reqd": 1
"columns": 0, },
"fieldname": "driver",
"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": "Driver",
"length": 0,
"no_copy": 0,
"options": "Driver",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fetch_from": "driver.full_name",
"allow_in_quick_entry": 0, "fieldname": "driver_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Driver Name",
"columns": 0, "read_only": 1
"fetch_from": "driver.full_name", },
"fieldname": "driver_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Driver Name",
"length": 0,
"no_copy": 0,
"options": "",
"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, "fieldname": "total_distance",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "label": "Total Estimated Distance",
"bold": 0, "precision": "2",
"collapsible": 0, "read_only": 1
"columns": 0, },
"fieldname": "total_distance",
"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": "Total Estimated Distance",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"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, "depends_on": "eval:doc.total_distance",
"allow_in_quick_entry": 0, "fieldname": "uom",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "Distance UOM",
"collapsible": 0, "options": "UOM",
"columns": 0, "read_only": 1
"default": "", },
"depends_on": "eval:doc.total_distance",
"fieldname": "uom",
"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": "Distance UOM",
"length": 0,
"no_copy": 0,
"options": "UOM",
"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, "fieldname": "column_break_4",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"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, "fieldname": "vehicle",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Vehicle",
"bold": 0, "options": "Vehicle",
"collapsible": 0, "reqd": 1
"columns": 0, },
"fieldname": "vehicle",
"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": "Vehicle",
"length": 0,
"no_copy": 0,
"options": "Vehicle",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "departure_time",
"allow_in_quick_entry": 0, "fieldtype": "Datetime",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Departure Time",
"collapsible": 0, "reqd": 1
"columns": 0, },
"fieldname": "departure_time",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Departure Time",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "delivery_service_stops",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Delivery Stops"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "delivery_service_stops",
"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": "Delivery Stops",
"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, "fieldname": "delivery_stops",
"allow_in_quick_entry": 0, "fieldtype": "Table",
"allow_on_submit": 0, "label": "Delivery Stop",
"bold": 0, "options": "Delivery Stop",
"collapsible": 0, "reqd": 1
"columns": 0, },
"fieldname": "delivery_stops",
"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": "Delivery Stop",
"length": 0,
"no_copy": 0,
"options": "Delivery Stop",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "depends_on": "eval:!doc.__islocal",
"allow_in_quick_entry": 0, "description": "Use Google Maps Direction API to calculate estimated arrival times",
"allow_on_submit": 0, "fieldname": "calculate_arrival_time",
"bold": 0, "fieldtype": "Button",
"collapsible": 0, "label": "Calculate Estimated Arrival Times"
"columns": 0, },
"depends_on": "eval:!doc.__islocal",
"fieldname": "calculate_arrival_time",
"fieldtype": "Button",
"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": "Calculate Estimated Arrival Times",
"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, "depends_on": "eval:!doc.__islocal",
"allow_in_quick_entry": 0, "description": "Use Google Maps Direction API to optimize route",
"allow_on_submit": 0, "fieldname": "optimize_route",
"bold": 0, "fieldtype": "Button",
"collapsible": 0, "label": "Optimize Route"
"columns": 0, },
"depends_on": "eval:!doc.__islocal",
"fieldname": "optimize_route",
"fieldtype": "Button",
"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": "Optimize Route",
"length": 0,
"no_copy": 0,
"options": "",
"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_on_submit": 1,
"allow_in_quick_entry": 0, "fieldname": "section_break_15",
"allow_on_submit": 1, "fieldtype": "Section Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_15",
"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, "fieldname": "status",
"allow_in_quick_entry": 0, "fieldtype": "Select",
"allow_on_submit": 0, "in_standard_filter": 1,
"bold": 0, "label": "Status",
"collapsible": 0, "no_copy": 1,
"columns": 0, "options": "Draft\nScheduled\nIn Transit\nCompleted\nCancelled",
"fieldname": "status", "print_hide": 1,
"fieldtype": "Select", "read_only": 1
"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, "fieldname": "cb_more_info",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"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, "fieldname": "amended_from",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Amended From",
"bold": 0, "no_copy": 1,
"collapsible": 0, "options": "Delivery Trip",
"columns": 0, "print_hide": 1,
"fieldname": "amended_from", "read_only": 1
"fieldtype": "Link", },
"hidden": 0, {
"ignore_user_permissions": 0, "fetch_from": "driver.address",
"ignore_xss_filter": 0, "fieldname": "driver_address",
"in_filter": 0, "fieldtype": "Link",
"in_global_search": 0, "label": "Driver Address",
"in_list_view": 0, "options": "Address",
"in_standard_filter": 0, "read_only": 1
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Delivery Trip",
"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
} }
], ],
"has_web_view": 0, "is_submittable": 1,
"hide_heading": 0, "modified": "2019-07-18 16:38:44.112651",
"hide_toolbar": 0, "modified_by": "Administrator",
"idx": 0, "module": "Stock",
"image_view": 0, "name": "Delivery Trip",
"in_create": 0, "owner": "Administrator",
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-10-22 08:25:42.323147",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Trip",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 1, "amend": 1,
"cancel": 1, "cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0, "print": 1,
"import": 0, "read": 1,
"permlevel": 0, "report": 1,
"print": 1, "role": "Fulfillment User",
"read": 1, "share": 1,
"report": 1, "submit": 1,
"role": "Fulfillment User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
"amend": 1, "amend": 1,
"cancel": 1, "cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0, "print": 1,
"import": 0, "read": 1,
"permlevel": 0, "report": 1,
"print": 1, "role": "Stock User",
"read": 1, "share": 1,
"report": 1, "submit": 1,
"role": "Stock User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "DESC",
"read_only_onload": 0, "title_field": "driver_name"
"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
} }

View File

@ -100,16 +100,13 @@ class DeliveryTrip(Document):
optimize (bool): True if route needs to be optimized, else False optimize (bool): True if route needs to be optimized, else False
""" """
if not frappe.db.get_single_value("Google Maps Settings", "enabled"):
frappe.throw(_("Cannot process route, since Google Maps Settings is disabled."))
departure_datetime = get_datetime(self.departure_time) departure_datetime = get_datetime(self.departure_time)
route_list = self.form_route_list(optimize) route_list = self.form_route_list(optimize)
# For locks, maintain idx count while looping through route list # For locks, maintain idx count while looping through route list
idx = 0 idx = 0
for route in route_list: for route in route_list:
directions = get_directions(route, optimize) directions = self.get_directions(route, optimize)
if directions: if directions:
if optimize and len(directions.get("waypoint_order")) > 1: if optimize and len(directions.get("waypoint_order")) > 1:
@ -157,9 +154,10 @@ class DeliveryTrip(Document):
Returns: Returns:
(list of list of str): List of address routes split at locks, if optimize is `True` (list of list of str): List of address routes split at locks, if optimize is `True`
""" """
if not self.driver_address:
frappe.throw(_("Cannot Calculate Arrival Time as Driver Address is Missing."))
settings = frappe.get_single("Google Maps Settings") home_address = get_address_display(frappe.get_doc("Address", self.driver_address).as_dict())
home_address = get_address_display(frappe.get_doc("Address", settings.home_address).as_dict())
route_list = [] route_list = []
# Initialize first leg with origin as the home address # Initialize first leg with origin as the home address
@ -204,6 +202,47 @@ class DeliveryTrip(Document):
self.delivery_stops[start:start + len(stops_order)] = stops_order self.delivery_stops[start:start + len(stops_order)] = stops_order
def get_directions(self, route, optimize):
"""
Retrieve map directions for a given route and departure time.
If optimize is `True`, Google Maps will return an optimized
order for the intermediate waypoints.
NOTE: Google's API does take an additional `departure_time` key,
but it only works for routes without any waypoints.
Args:
route (list of str): Route addresses (origin -> waypoint(s), if any -> destination)
optimize (bool): `True` if route needs to be optimized, else `False`
Returns:
(dict): Route legs and, if `optimize` is `True`, optimized waypoint order
"""
if not frappe.db.get_single_value("Google Settings", "api_key"):
frappe.throw(_("Enter API key in Google Settings."))
import googlemaps
try:
maps_client = googlemaps.Client(key=frappe.db.get_single_value("Google Settings", "api_key"))
except Exception as e:
frappe.throw(e)
directions_data = {
"origin": route[0],
"destination": route[-1],
"waypoints": route[1: -1],
"optimize_waypoints": optimize
}
try:
directions = maps_client.directions(**directions_data)
except Exception as e:
frappe.throw(_(e.message))
return directions[0] if directions else False
@frappe.whitelist() @frappe.whitelist()
def get_contact_and_address(name): def get_contact_and_address(name):
@ -278,18 +317,6 @@ def get_contact_display(contact):
return contact_info.html return contact_info.html
@frappe.whitelist()
def optimize_route(delivery_trip):
delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)
delivery_trip.process_route(optimize=True)
@frappe.whitelist()
def get_arrival_times(delivery_trip):
delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)
delivery_trip.process_route(optimize=False)
def sanitize_address(address): def sanitize_address(address):
""" """
Remove HTML breaks in a given address Remove HTML breaks in a given address
@ -310,41 +337,6 @@ def sanitize_address(address):
return ', '.join(address[:3]) return ', '.join(address[:3])
def get_directions(route, optimize):
"""
Retrieve map directions for a given route and departure time.
If optimize is `True`, Google Maps will return an optimized
order for the intermediate waypoints.
NOTE: Google's API does take an additional `departure_time` key,
but it only works for routes without any waypoints.
Args:
route (list of str): Route addresses (origin -> waypoint(s), if any -> destination)
optimize (bool): `True` if route needs to be optimized, else `False`
Returns:
(dict): Route legs and, if `optimize` is `True`, optimized waypoint order
"""
settings = frappe.get_single("Google Maps Settings")
maps_client = settings.get_client()
directions_data = {
"origin": route[0],
"destination": route[-1],
"waypoints": route[1: -1],
"optimize_waypoints": optimize
}
try:
directions = maps_client.directions(**directions_data)
except Exception as e:
frappe.throw(_(e.message))
return directions[0] if directions else False
@frappe.whitelist() @frappe.whitelist()
def notify_customers(delivery_trip): def notify_customers(delivery_trip):
delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip) delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)

View File

@ -14,16 +14,13 @@ from frappe.utils import add_days, flt, now_datetime, nowdate
class TestDeliveryTrip(unittest.TestCase): class TestDeliveryTrip(unittest.TestCase):
def setUp(self): def setUp(self):
create_driver() driver = create_driver()
create_vehicle() create_vehicle()
create_delivery_notification() create_delivery_notification()
create_test_contact_and_address() create_test_contact_and_address()
address = create_address(driver)
settings = frappe.get_single("Google Maps Settings") self.delivery_trip = create_delivery_trip(driver, address)
settings.home_address = frappe.get_last_doc("Address").name
settings.save()
self.delivery_trip = create_delivery_trip()
def tearDown(self): def tearDown(self):
frappe.db.sql("delete from `tabDriver`") frappe.db.sql("delete from `tabDriver`")
@ -99,17 +96,42 @@ class TestDeliveryTrip(unittest.TestCase):
self.delivery_trip.save() self.delivery_trip.save()
self.assertEqual(self.delivery_trip.status, "Completed") self.assertEqual(self.delivery_trip.status, "Completed")
def create_address(driver):
if not frappe.db.exists("Address", {"address_title": "_Test Address for Driver"}):
address = frappe.get_doc({
"doctype": "Address",
"address_title": "_Test Address for Driver",
"address_type": "Office",
"address_line1": "Station Road",
"city": "_Test City",
"state": "Test State",
"country": "India",
"links":[
{
"link_doctype": "Driver",
"link_name": driver.name
}
]
}).insert(ignore_permissions=True)
frappe.db.set_value("Driver", driver.name, "address", address.name)
return address
return frappe.get_doc("Address", {"address_title": "_Test Address for Driver"})
def create_driver(): def create_driver():
if not frappe.db.exists("Driver", "Newton Scmander"): if not frappe.db.exists("Driver", {"full_name": "Newton Scmander"}):
driver = frappe.get_doc({ driver = frappe.get_doc({
"doctype": "Driver", "doctype": "Driver",
"full_name": "Newton Scmander", "full_name": "Newton Scmander",
"cell_number": "98343424242", "cell_number": "98343424242",
"license_number": "B809" "license_number": "B809",
}) }).insert(ignore_permissions=True)
driver.insert()
return driver
return frappe.get_doc("Driver", {"full_name": "Newton Scmander"})
def create_delivery_notification(): def create_delivery_notification():
if not frappe.db.exists("Email Template", "Delivery Notification"): if not frappe.db.exists("Email Template", "Delivery Notification"):
@ -144,16 +166,16 @@ def create_vehicle():
vehicle.insert() vehicle.insert()
def create_delivery_trip(contact=None): def create_delivery_trip(driver, address, contact=None):
if not contact: if not contact:
contact = get_contact_and_address("_Test Customer") contact = get_contact_and_address("_Test Customer")
delivery_trip = frappe.new_doc("Delivery Trip") delivery_trip = frappe.get_doc({
delivery_trip.update({
"doctype": "Delivery Trip", "doctype": "Delivery Trip",
"company": erpnext.get_default_company(), "company": erpnext.get_default_company(),
"departure_time": add_days(now_datetime(), 5), "departure_time": add_days(now_datetime(), 5),
"driver": frappe.db.get_value('Driver', {"full_name": "Newton Scmander"}), "driver": driver.name,
"driver_address": address.name,
"vehicle": "JB 007", "vehicle": "JB 007",
"delivery_stops": [{ "delivery_stops": [{
"customer": "_Test Customer", "customer": "_Test Customer",
@ -165,7 +187,6 @@ def create_delivery_trip(contact=None):
"address": contact.shipping_address.parent, "address": contact.shipping_address.parent,
"contact": contact.contact_person.parent "contact": contact.contact_person.parent
}] }]
}) }).insert(ignore_permissions=True)
delivery_trip.insert()
return delivery_trip return delivery_trip

View File

@ -197,6 +197,9 @@ class Item(WebsiteGenerator):
'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5))
def validate_website_image(self): def validate_website_image(self):
if frappe.flags.in_import:
return
"""Validate if the website image is a public file""" """Validate if the website image is a public file"""
auto_set_website_image = False auto_set_website_image = False
if not self.website_image and self.image: if not self.website_image and self.image:
@ -216,8 +219,7 @@ class Item(WebsiteGenerator):
if not file_doc: if not file_doc:
if not auto_set_website_image: if not auto_set_website_image:
frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found") frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name))
.format(self.website_image, self.name))
self.website_image = None self.website_image = None
@ -228,6 +230,9 @@ class Item(WebsiteGenerator):
self.website_image = None self.website_image = None
def make_thumbnail(self): def make_thumbnail(self):
if frappe.flags.in_import:
return
"""Make a thumbnail of `website_image`""" """Make a thumbnail of `website_image`"""
import requests.exceptions import requests.exceptions

View File

@ -20,6 +20,8 @@ frappe.listview_settings['Material Request'] = {
return [__("Issued"), "green", "per_ordered,=,100"]; return [__("Issued"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Customer Provided") { } else if (doc.material_request_type == "Customer Provided") {
return [__("Received"), "green", "per_ordered,=,100"]; return [__("Received"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Manufacture") {
return [__("Manufactured"), "green", "per_ordered,=,100"];
} }
} }
} }

View File

@ -264,8 +264,12 @@ frappe.ui.form.on('Purchase Receipt Item', {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
frappe.db.get_value('Item', {name: d.item_code}, 'sample_quantity', (r) => { frappe.db.get_value('Item', {name: d.item_code}, 'sample_quantity', (r) => {
frappe.model.set_value(cdt, cdn, "sample_quantity", r.sample_quantity); frappe.model.set_value(cdt, cdn, "sample_quantity", r.sample_quantity);
validate_sample_quantity(frm, cdt, cdn);
}); });
}, },
qty: function(frm, cdt, cdn) {
validate_sample_quantity(frm, cdt, cdn);
},
sample_quantity: function(frm, cdt, cdn) { sample_quantity: function(frm, cdt, cdn) {
validate_sample_quantity(frm, cdt, cdn); validate_sample_quantity(frm, cdt, cdn);
}, },
@ -283,7 +287,7 @@ cur_frm.cscript['Make Stock Entry'] = function() {
var validate_sample_quantity = function(frm, cdt, cdn) { var validate_sample_quantity = function(frm, cdt, cdn) {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
if (d.sample_quantity) { if (d.sample_quantity && d.qty) {
frappe.call({ frappe.call({
method: 'erpnext.stock.doctype.stock_entry.stock_entry.validate_sample_quantity', method: 'erpnext.stock.doctype.stock_entry.stock_entry.validate_sample_quantity',
args: { args: {

View File

@ -222,7 +222,7 @@ def validate_serial_no(sle, item_det):
frappe.throw(_("Serial No {0} has already been received").format(serial_no), frappe.throw(_("Serial No {0} has already been received").format(serial_no),
SerialNoDuplicateError) SerialNoDuplicateError)
if (sr.delivery_document_no and sle.voucher_type != 'Stock Entry' if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation']
and sle.voucher_type == sr.delivery_document_type): and sle.voucher_type == sr.delivery_document_type):
return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, 'return_against') return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, 'return_against')
if return_against and return_against != sr.delivery_document_no: if return_against and return_against != sr.delivery_document_no:
@ -299,7 +299,7 @@ def validate_so_serial_no(sr, sales_order,):
be delivered""").format(sales_order, sr.item_code, sr.name)) be delivered""").format(sales_order, sr.item_code, sr.name))
def has_duplicate_serial_no(sn, sle): def has_duplicate_serial_no(sn, sle):
if sn.warehouse: if sn.warehouse and sle.voucher_type != 'Stock Reconciliation':
return True return True
if sn.company != sle.company: if sn.company != sle.company:
@ -415,16 +415,20 @@ def update_serial_nos_after_submit(controller, parentfield):
if not stock_ledger_entries: return if not stock_ledger_entries: return
for d in controller.get(parentfield): for d in controller.get(parentfield):
if d.serial_no:
continue
update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice")
and d.rejected_qty) else False and d.rejected_qty) else False
accepted_serial_nos_updated = False accepted_serial_nos_updated = False
if controller.doctype == "Stock Entry": if controller.doctype == "Stock Entry":
warehouse = d.t_warehouse warehouse = d.t_warehouse
qty = d.transfer_qty qty = d.transfer_qty
else: else:
warehouse = d.warehouse warehouse = d.warehouse
qty = d.stock_qty qty = (d.qty if controller.doctype == "Stock Reconciliation"
else d.stock_qty)
for sle in stock_ledger_entries: for sle in stock_ledger_entries:
if sle.voucher_detail_no==d.name: if sle.voucher_detail_no==d.name:
if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \ if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \

View File

@ -359,7 +359,7 @@ class StockEntry(StockController):
d.basic_rate = 0.0 d.basic_rate = 0.0
elif d.t_warehouse and not d.basic_rate: elif d.t_warehouse and not d.basic_rate:
d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse, d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
self.doctype, d.name, d.allow_zero_valuation_rate, self.doctype, self.name, d.allow_zero_valuation_rate,
currency=erpnext.get_company_currency(self.company)) currency=erpnext.get_company_currency(self.company))
def set_actual_qty(self): def set_actual_qty(self):

View File

@ -38,7 +38,7 @@ class StockLedgerEntry(Document):
self.check_stock_frozen_date() self.check_stock_frozen_date()
self.actual_amt_check() self.actual_amt_check()
if not self.get("via_landed_cost_voucher") and self.voucher_type != 'Stock Reconciliation': if not self.get("via_landed_cost_voucher"):
from erpnext.stock.doctype.serial_no.serial_no import process_serial_no from erpnext.stock.doctype.serial_no.serial_no import process_serial_no
process_serial_no(self) process_serial_no(self)

View File

@ -12,8 +12,7 @@ frappe.ui.form.on("Stock Reconciliation", {
return { return {
query: "erpnext.controllers.queries.item_query", query: "erpnext.controllers.queries.item_query",
filters:{ filters:{
"is_stock_item": 1, "is_stock_item": 1
"has_serial_no": 0
} }
} }
}); });
@ -77,6 +76,7 @@ frappe.ui.form.on("Stock Reconciliation", {
set_valuation_rate_and_qty: function(frm, cdt, cdn) { set_valuation_rate_and_qty: function(frm, cdt, cdn) {
var d = frappe.model.get_doc(cdt, cdn); var d = frappe.model.get_doc(cdt, cdn);
if(d.item_code && d.warehouse) { if(d.item_code && d.warehouse) {
frappe.call({ frappe.call({
method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_stock_balance_for", method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_stock_balance_for",
@ -84,7 +84,8 @@ frappe.ui.form.on("Stock Reconciliation", {
item_code: d.item_code, item_code: d.item_code,
warehouse: d.warehouse, warehouse: d.warehouse,
posting_date: frm.doc.posting_date, posting_date: frm.doc.posting_date,
posting_time: frm.doc.posting_time posting_time: frm.doc.posting_time,
batch_no: d.batch_no
}, },
callback: function(r) { callback: function(r) {
frappe.model.set_value(cdt, cdn, "qty", r.message.qty); frappe.model.set_value(cdt, cdn, "qty", r.message.qty);
@ -93,7 +94,7 @@ frappe.ui.form.on("Stock Reconciliation", {
frappe.model.set_value(cdt, cdn, "current_valuation_rate", r.message.rate); frappe.model.set_value(cdt, cdn, "current_valuation_rate", r.message.rate);
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
} }
}); });
} }
@ -152,17 +153,44 @@ frappe.ui.form.on("Stock Reconciliation Item", {
barcode: function(frm, cdt, cdn) { barcode: function(frm, cdt, cdn) {
frm.events.set_item_code(frm, cdt, cdn); frm.events.set_item_code(frm, cdt, cdn);
}, },
warehouse: function(frm, cdt, cdn) { warehouse: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if (child.batch_no) {
frappe.model.set_value(child.cdt, child.cdn, "batch_no", "");
}
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
}, },
item_code: function(frm, cdt, cdn) { item_code: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if (child.batch_no) {
frappe.model.set_value(cdt, cdn, "batch_no", "");
}
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
}, },
batch_no: function(frm, cdt, cdn) {
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
},
qty: function(frm, cdt, cdn) { qty: function(frm, cdt, cdn) {
frm.events.set_amount_quantity(frm, cdt, cdn); frm.events.set_amount_quantity(frm, cdt, cdn);
}, },
valuation_rate: function(frm, cdt, cdn) { valuation_rate: function(frm, cdt, cdn) {
frm.events.set_amount_quantity(frm, cdt, cdn); frm.events.set_amount_quantity(frm, cdt, cdn);
},
serial_no: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if (child.serial_no) {
const serial_nos = child.serial_no.trim().split('\n');
frappe.model.set_value(cdt, cdn, "qty", serial_nos.length);
}
} }
}); });

View File

@ -9,7 +9,9 @@ from frappe.utils import cstr, flt, cint
from erpnext.stock.stock_ledger import update_entries_after from erpnext.stock.stock_ledger import update_entries_after
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController
from erpnext.accounts.utils import get_company_default from erpnext.accounts.utils import get_company_default
from erpnext.stock.utils import get_stock_balance from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos
from erpnext.stock.doctype.batch.batch import get_batch_qty
class OpeningEntryAccountError(frappe.ValidationError): pass class OpeningEntryAccountError(frappe.ValidationError): pass
class EmptyStockReconciliationItemsError(frappe.ValidationError): pass class EmptyStockReconciliationItemsError(frappe.ValidationError): pass
@ -30,10 +32,16 @@ class StockReconciliation(StockController):
self.validate_expense_account() self.validate_expense_account()
self.set_total_qty_and_amount() self.set_total_qty_and_amount()
if self._action=="submit":
self.make_batches('warehouse')
def on_submit(self): def on_submit(self):
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries() self.make_gl_entries()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
def on_cancel(self): def on_cancel(self):
self.delete_and_repost_sle() self.delete_and_repost_sle()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
@ -42,23 +50,28 @@ class StockReconciliation(StockController):
"""Remove items if qty or rate is not changed""" """Remove items if qty or rate is not changed"""
self.difference_amount = 0.0 self.difference_amount = 0.0
def _changed(item): def _changed(item):
qty, rate = get_stock_balance(item.item_code, item.warehouse, item_dict = get_stock_balance_for(item.item_code, item.warehouse,
self.posting_date, self.posting_time, with_valuation_rate=True) self.posting_date, self.posting_time, batch_no=item.batch_no)
if (item.qty==None or item.qty==qty) and (item.valuation_rate==None or item.valuation_rate==rate): if (((item.qty is None or item.qty==item_dict.get("qty")) and
(item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and not item.serial_no)
or (item.serial_no and item.serial_no == item_dict.get("serial_nos"))):
return False return False
else: else:
# set default as current rates # set default as current rates
if item.qty==None: if item.qty is None:
item.qty = qty item.qty = item_dict.get("qty")
if item.valuation_rate==None: if item.valuation_rate is None:
item.valuation_rate = rate item.valuation_rate = item_dict.get("rate")
item.current_qty = qty if item_dict.get("serial_nos"):
item.current_valuation_rate = rate item.current_serial_no = item_dict.get("serial_nos")
item.current_qty = item_dict.get("qty")
item.current_valuation_rate = item_dict.get("rate")
self.difference_amount += (flt(item.qty, item.precision("qty")) * \ self.difference_amount += (flt(item.qty, item.precision("qty")) * \
flt(item.valuation_rate or rate, item.precision("valuation_rate")) \ flt(item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")) \
- flt(qty, item.precision("qty")) * flt(rate, item.precision("valuation_rate"))) - flt(item_dict.get("qty"), item.precision("qty")) * flt(item_dict.get("rate"), item.precision("valuation_rate")))
return True return True
items = list(filter(lambda d: _changed(d), self.items)) items = list(filter(lambda d: _changed(d), self.items))
@ -84,12 +97,17 @@ class StockReconciliation(StockController):
for row_num, row in enumerate(self.items): for row_num, row in enumerate(self.items):
# find duplicates # find duplicates
if [row.item_code, row.warehouse] in item_warehouse_combinations: key = [row.item_code, row.warehouse]
for field in ['serial_no', 'batch_no']:
if row.get(field):
key.append(row.get(field))
if key in item_warehouse_combinations:
self.validation_messages.append(_get_msg(row_num, _("Duplicate entry"))) self.validation_messages.append(_get_msg(row_num, _("Duplicate entry")))
else: else:
item_warehouse_combinations.append([row.item_code, row.warehouse]) item_warehouse_combinations.append(key)
self.validate_item(row.item_code, row_num+1) self.validate_item(row.item_code, row)
# validate warehouse # validate warehouse
if not frappe.db.get_value("Warehouse", row.warehouse): if not frappe.db.get_value("Warehouse", row.warehouse):
@ -131,7 +149,7 @@ class StockReconciliation(StockController):
raise frappe.ValidationError(self.validation_messages) raise frappe.ValidationError(self.validation_messages)
def validate_item(self, item_code, row_num): def validate_item(self, item_code, row):
from erpnext.stock.doctype.item.item import validate_end_of_life, \ from erpnext.stock.doctype.item.item import validate_end_of_life, \
validate_is_stock_item, validate_cancelled_item validate_is_stock_item, validate_cancelled_item
@ -145,51 +163,130 @@ class StockReconciliation(StockController):
validate_is_stock_item(item_code, item.is_stock_item, verbose=0) validate_is_stock_item(item_code, item.is_stock_item, verbose=0)
# item should not be serialized # item should not be serialized
if item.has_serial_no == 1: if item.has_serial_no and not row.serial_no and not item.serial_no_series:
raise frappe.ValidationError(_("Serialized Item {0} cannot be updated using Stock Reconciliation, please use Stock Entry").format(item_code)) raise frappe.ValidationError(_("Serial no(s) required for serialized item {0}").format(item_code))
# item managed batch-wise not allowed # item managed batch-wise not allowed
if item.has_batch_no == 1: if item.has_batch_no and not row.batch_no and not item.create_new_batch:
raise frappe.ValidationError(_("Batched Item {0} cannot be updated using Stock Reconciliation, instead use Stock Entry").format(item_code)) raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code))
# docstatus should be < 2 # docstatus should be < 2
validate_cancelled_item(item_code, item.docstatus, verbose=0) validate_cancelled_item(item_code, item.docstatus, verbose=0)
except Exception as e: except Exception as e:
self.validation_messages.append(_("Row # ") + ("%d: " % (row_num)) + cstr(e)) self.validation_messages.append(_("Row # ") + ("%d: " % (row.idx)) + cstr(e))
def update_stock_ledger(self): def update_stock_ledger(self):
""" find difference between current and expected entries """ find difference between current and expected entries
and create stock ledger entries based on the difference""" and create stock ledger entries based on the difference"""
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle
sl_entries = []
for row in self.items: for row in self.items:
item = frappe.get_doc("Item", row.item_code)
if item.has_serial_no or item.has_batch_no:
self.get_sle_for_serialized_items(row, sl_entries)
else:
previous_sle = get_previous_sle({
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time
})
if previous_sle:
if row.qty in ("", None):
row.qty = previous_sle.get("qty_after_transaction", 0)
if row.valuation_rate in ("", None):
row.valuation_rate = previous_sle.get("valuation_rate", 0)
if row.qty and not row.valuation_rate:
frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx))
if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction")
and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0))
or (not previous_sle and not row.qty)):
continue
sl_entries.append(self.get_sle_for_items(row))
if sl_entries:
self.make_sl_entries(sl_entries)
def get_sle_for_serialized_items(self, row, sl_entries):
from erpnext.stock.stock_ledger import get_previous_sle
serial_nos = get_serial_nos(row.serial_no)
# To issue existing serial nos
if row.current_qty and (row.current_serial_no or row.batch_no):
args = self.get_sle_for_items(row)
args.update({
'actual_qty': -1 * row.current_qty,
'serial_no': row.current_serial_no,
'batch_no': row.batch_no,
'valuation_rate': row.current_valuation_rate
})
if row.current_serial_no:
args.update({
'qty_after_transaction': 0,
})
sl_entries.append(args)
for serial_no in serial_nos:
args = self.get_sle_for_items(row, [serial_no])
previous_sle = get_previous_sle({ previous_sle = get_previous_sle({
"item_code": row.item_code, "item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time "posting_time": self.posting_time,
"serial_no": serial_no
}) })
if previous_sle:
if row.qty in ("", None):
row.qty = previous_sle.get("qty_after_transaction", 0)
if row.valuation_rate in ("", None): if previous_sle and row.warehouse != previous_sle.get("warehouse"):
row.valuation_rate = previous_sle.get("valuation_rate", 0) # If serial no exists in different warehouse
if row.qty and not row.valuation_rate: new_args = args.copy()
frappe.throw(_("Valuation Rate required for Item in row {0}").format(row.idx)) new_args.update({
'actual_qty': -1,
'qty_after_transaction': cint(previous_sle.get('qty_after_transaction')) - 1,
'warehouse': previous_sle.get("warehouse", '') or row.warehouse,
'valuation_rate': previous_sle.get("valuation_rate")
})
if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") sl_entries.append(new_args)
and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0))
or (not previous_sle and not row.qty)):
continue
self.insert_entries(row) if row.qty:
args = self.get_sle_for_items(row)
def insert_entries(self, row): args.update({
'actual_qty': row.qty,
'incoming_rate': row.valuation_rate,
'valuation_rate': row.valuation_rate
})
sl_entries.append(args)
if serial_nos == get_serial_nos(row.current_serial_no):
# update valuation rate
self.update_valuation_rate_for_serial_nos(row, serial_nos)
def update_valuation_rate_for_serial_nos(self, row, serial_nos):
valuation_rate = row.valuation_rate if self.docstatus == 1 else row.current_valuation_rate
for d in serial_nos:
frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate)
def get_sle_for_items(self, row, serial_nos=None):
"""Insert Stock Ledger Entries""" """Insert Stock Ledger Entries"""
args = frappe._dict({
if not serial_nos and row.serial_no:
serial_nos = get_serial_nos(row.serial_no)
data = frappe._dict({
"doctype": "Stock Ledger Entry", "doctype": "Stock Ledger Entry",
"item_code": row.item_code, "item_code": row.item_code,
"warehouse": row.warehouse, "warehouse": row.warehouse,
@ -197,13 +294,19 @@ class StockReconciliation(StockController):
"posting_time": self.posting_time, "posting_time": self.posting_time,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"voucher_detail_no": row.name,
"company": self.company, "company": self.company,
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
"is_cancelled": "No", "is_cancelled": "No" if self.docstatus != 2 else "Yes",
"qty_after_transaction": flt(row.qty, row.precision("qty")), "serial_no": '\n'.join(serial_nos) if serial_nos else '',
"batch_no": row.batch_no,
"valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")) "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate"))
}) })
self.make_sl_entries([args])
if not row.batch_no:
data.qty_after_transaction = flt(row.qty, row.precision("qty"))
return data
def delete_and_repost_sle(self): def delete_and_repost_sle(self):
""" Delete Stock Ledger Entries related to this voucher """ Delete Stock Ledger Entries related to this voucher
@ -217,6 +320,15 @@ class StockReconciliation(StockController):
frappe.db.sql("""delete from `tabStock Ledger Entry` frappe.db.sql("""delete from `tabStock Ledger Entry`
where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)) where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name))
sl_entries = []
for row in self.items:
if row.serial_no or row.batch_no or row.current_serial_no:
self.get_sle_for_serialized_items(row, sl_entries)
if sl_entries:
sl_entries.reverse()
self.make_sl_entries(sl_entries)
# repost future entries for selected item_code, warehouse # repost future entries for selected item_code, warehouse
for entries in existing_entries: for entries in existing_entries:
update_entries_after({ update_entries_after({
@ -310,17 +422,52 @@ def get_items(warehouse, posting_date, posting_time, company):
return res return res
@frappe.whitelist() @frappe.whitelist()
def get_stock_balance_for(item_code, warehouse, posting_date, posting_time): def get_stock_balance_for(item_code, warehouse,
posting_date, posting_time, batch_no=None, with_valuation_rate= True):
frappe.has_permission("Stock Reconciliation", "write", throw = True) frappe.has_permission("Stock Reconciliation", "write", throw = True)
qty, rate = get_stock_balance(item_code, warehouse, item_dict = frappe.db.get_value("Item", item_code,
posting_date, posting_time, with_valuation_rate=True) ["has_serial_no", "has_batch_no"], as_dict=1)
serial_nos = ""
if item_dict.get("has_serial_no"):
qty, rate, serial_nos = get_qty_rate_for_serial_nos(item_code,
warehouse, posting_date, posting_time, item_dict)
else:
qty, rate = get_stock_balance(item_code, warehouse,
posting_date, posting_time, with_valuation_rate=with_valuation_rate)
if item_dict.get("has_batch_no"):
qty = get_batch_qty(batch_no, warehouse) or 0
return { return {
'qty': qty, 'qty': qty,
'rate': rate 'rate': rate,
'serial_nos': serial_nos
} }
def get_qty_rate_for_serial_nos(item_code, warehouse, posting_date, posting_time, item_dict):
args = {
"item_code": item_code,
"warehouse": warehouse,
"posting_date": posting_date,
"posting_time": posting_time,
}
serial_nos_list = [serial_no.get("name")
for serial_no in get_available_serial_nos(item_code, warehouse)]
qty = len(serial_nos_list)
serial_nos = '\n'.join(serial_nos_list)
args.update({
'qty': qty,
"serial_nos": serial_nos
})
rate = get_incoming_rate(args, raise_error_if_no_rate=False) or 0
return qty, rate, serial_nos
@frappe.whitelist() @frappe.whitelist()
def get_difference_account(purpose, company): def get_difference_account(purpose, company):
if purpose == 'Stock Reconciliation': if purpose == 'Stock Reconciliation':

View File

@ -13,9 +13,12 @@ from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos, get_stock_value_on
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class TestStockReconciliation(unittest.TestCase): class TestStockReconciliation(unittest.TestCase):
def setUp(self): def setUp(self):
create_batch_or_serial_no_items()
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
self.insert_existing_sle() self.insert_existing_sle()
@ -106,6 +109,135 @@ class TestStockReconciliation(unittest.TestCase):
make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item", make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item",
target="_Test Warehouse - _TC", qty=15, basic_rate=1200) target="_Test Warehouse - _TC", qty=15, basic_rate=1200)
def test_stock_reco_for_serialized_item(self):
set_perpetual_inventory()
to_delete_records = []
to_delete_serial_nos = []
# Add new serial nos
serial_item_code = "Stock-Reco-Serial-Item-1"
serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC"
sr = create_stock_reconciliation(item_code=serial_item_code,
warehouse = serial_warehouse, qty=5, rate=200)
# print(sr.name)
serial_nos = get_serial_nos(sr.items[0].serial_no)
self.assertEqual(len(serial_nos), 5)
args = {
"item_code": serial_item_code,
"warehouse": serial_warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
"serial_no": sr.items[0].serial_no
}
valuation_rate = get_incoming_rate(args)
self.assertEqual(valuation_rate, 200)
to_delete_records.append(sr.name)
sr = create_stock_reconciliation(item_code=serial_item_code,
warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos))
# print(sr.name)
serial_nos1 = get_serial_nos(sr.items[0].serial_no)
self.assertEqual(len(serial_nos1), 5)
args = {
"item_code": serial_item_code,
"warehouse": serial_warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
"serial_no": sr.items[0].serial_no
}
valuation_rate = get_incoming_rate(args)
self.assertEqual(valuation_rate, 300)
to_delete_records.append(sr.name)
to_delete_records.reverse()
for d in to_delete_records:
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
frappe.delete_doc("Stock Reconciliation", stock_doc.name)
for d in serial_nos + serial_nos1:
if frappe.db.exists("Serial No", d):
frappe.delete_doc("Serial No", d)
def test_stock_reco_for_batch_item(self):
set_perpetual_inventory()
to_delete_records = []
to_delete_serial_nos = []
# Add new serial nos
item_code = "Stock-Reco-batch-Item-1"
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
sr = create_stock_reconciliation(item_code=item_code,
warehouse = warehouse, qty=5, rate=200, do_not_submit=1)
sr.save(ignore_permissions=True)
sr.submit()
self.assertTrue(sr.items[0].batch_no)
to_delete_records.append(sr.name)
sr1 = create_stock_reconciliation(item_code=item_code,
warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no)
args = {
"item_code": item_code,
"warehouse": warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
}
valuation_rate = get_incoming_rate(args)
self.assertEqual(valuation_rate, 300)
to_delete_records.append(sr1.name)
sr2 = create_stock_reconciliation(item_code=item_code,
warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no)
stock_value = get_stock_value_on(warehouse, nowdate(), item_code)
self.assertEqual(stock_value, 0)
to_delete_records.append(sr2.name)
to_delete_records.reverse()
for d in to_delete_records:
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
frappe.delete_doc("Batch", sr.items[0].batch_no)
for d in to_delete_records:
frappe.delete_doc("Stock Reconciliation", d)
def create_batch_or_serial_no_items():
create_warehouse("_Test Warehouse for Stock Reco1",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
create_warehouse("_Test Warehouse for Stock Reco2",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
serial_item_doc = create_item("Stock-Reco-Serial-Item-1", is_stock_item=1)
if not serial_item_doc.has_serial_no:
serial_item_doc.has_serial_no = 1
serial_item_doc.serial_no_series = "SRSI.####"
serial_item_doc.save(ignore_permissions=True)
batch_item_doc = create_item("Stock-Reco-batch-Item-1", is_stock_item=1)
if not batch_item_doc.has_batch_no:
batch_item_doc.has_batch_no = 1
batch_item_doc.create_new_batch = 1
serial_item_doc.batch_number_series = "BASR.#####"
batch_item_doc.save(ignore_permissions=True)
def create_stock_reconciliation(**args): def create_stock_reconciliation(**args):
args = frappe._dict(args) args = frappe._dict(args)
sr = frappe.new_doc("Stock Reconciliation") sr = frappe.new_doc("Stock Reconciliation")
@ -120,11 +252,14 @@ def create_stock_reconciliation(**args):
"item_code": args.item_code or "_Test Item", "item_code": args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty, "qty": args.qty,
"valuation_rate": args.rate "valuation_rate": args.rate,
"serial_no": args.serial_no,
"batch_no": args.batch_no
}) })
try: try:
sr.submit() if not args.do_not_submit:
sr.submit()
except EmptyStockReconciliationItemsError: except EmptyStockReconciliationItemsError:
pass pass
return sr return sr
@ -140,3 +275,4 @@ def set_valuation_method(item_code, valuation_method):
}, allow_negative_stock=1) }, allow_negative_stock=1)
test_dependencies = ["Item", "Warehouse"] test_dependencies = ["Item", "Warehouse"]

View File

@ -1,560 +1,182 @@
{ {
"allow_copy": 0, "creation": "2015-02-17 01:06:05.072764",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "document_type": "Other",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2015-02-17 01:06:05.072764", "field_order": [
"custom": 0, "barcode",
"docstatus": 0, "item_code",
"doctype": "DocType", "item_name",
"document_type": "Other", "warehouse",
"editable_grid": 1, "column_break_6",
"engine": "InnoDB", "qty",
"valuation_rate",
"amount",
"serial_no_and_batch_section",
"serial_no",
"column_break_11",
"batch_no",
"section_break_3",
"current_qty",
"current_serial_no",
"column_break_9",
"current_valuation_rate",
"current_amount",
"section_break_14",
"quantity_difference",
"column_break_16",
"amount_difference"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "barcode",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "label": "Barcode",
"collapsible": 0, "print_hide": 1
"columns": 0, },
"fieldname": "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": "Barcode",
"length": 0,
"no_copy": 0,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 3,
"allow_on_submit": 0, "fieldname": "item_code",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_global_search": 1,
"columns": 3, "in_list_view": 1,
"fieldname": "item_code", "label": "Item Code",
"fieldtype": "Link", "options": "Item",
"hidden": 0, "reqd": 1
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"options": "Item",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "item_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_global_search": 1,
"collapsible": 0, "label": "Item Name",
"columns": 0, "no_copy": 1,
"fieldname": "item_name", "print_hide": 1,
"fieldtype": "Data", "read_only": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Item Name",
"length": 0,
"no_copy": 1,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 3,
"allow_on_submit": 0, "fieldname": "warehouse",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 3, "label": "Warehouse",
"fieldname": "warehouse", "options": "Warehouse",
"fieldtype": "Link", "reqd": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "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": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_6",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_6",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_on_submit": 0, "fieldname": "qty",
"bold": 0, "fieldtype": "Float",
"collapsible": 0, "in_list_view": 1,
"columns": 2, "label": "Quantity"
"description": "", },
"fieldname": "qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Quantity",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_on_submit": 0, "fieldname": "valuation_rate",
"bold": 0, "fieldtype": "Currency",
"collapsible": 0, "in_list_view": 1,
"columns": 2, "label": "Valuation Rate",
"description": "", "options": "Company:company:default_currency"
"fieldname": "valuation_rate", },
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Valuation Rate",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "amount",
"allow_on_submit": 0, "fieldtype": "Currency",
"bold": 0, "label": "Amount",
"collapsible": 0, "options": "Company:company:default_currency",
"columns": 0, "read_only": 1
"fieldname": "amount", },
"fieldtype": "Currency",
"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": "Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "serial_no_and_batch_section",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Serial No and Batch"
"collapsible": 0, },
"columns": 0,
"fieldname": "section_break_3",
"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": "Before reconciliation",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "serial_no",
"allow_on_submit": 0, "fieldtype": "Small Text",
"bold": 0, "label": "Serial No"
"collapsible": 0, },
"columns": 0,
"description": "",
"fieldname": "current_qty",
"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": "Current Qty",
"length": 0,
"no_copy": 0,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_11",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_9",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_3",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Before reconciliation"
"collapsible": 0, },
"columns": 0,
"description": "",
"fieldname": "current_valuation_rate",
"fieldtype": "Currency",
"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": "Current Valuation Rate",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0, "fieldname": "current_qty",
"bold": 0, "fieldtype": "Float",
"collapsible": 0, "label": "Current Qty",
"columns": 0, "read_only": 1
"description": "", },
"fieldname": "current_amount",
"fieldtype": "Currency",
"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": "Current Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "current_serial_no",
"allow_on_submit": 0, "fieldtype": "Small Text",
"bold": 0, "label": "Current Serial No",
"collapsible": 0, "no_copy": 1,
"columns": 0, "print_hide": 1,
"fieldname": "section_break_14", "read_only": 1
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_9",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "quantity_difference",
"fieldtype": "Read Only",
"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": "Quantity Difference",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "current_valuation_rate",
"allow_on_submit": 0, "fieldtype": "Currency",
"bold": 0, "label": "Current Valuation Rate",
"collapsible": 0, "options": "Company:company:default_currency",
"columns": 0, "read_only": 1
"fieldname": "column_break_16", },
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "current_amount",
"allow_on_submit": 0, "fieldtype": "Currency",
"bold": 0, "label": "Current Amount",
"collapsible": 0, "options": "Company:company:default_currency",
"columns": 0, "read_only": 1
"fieldname": "amount_difference", },
"fieldtype": "Currency", {
"hidden": 0, "fieldname": "section_break_14",
"ignore_user_permissions": 0, "fieldtype": "Section Break"
"ignore_xss_filter": 0, },
"in_filter": 0, {
"in_global_search": 0, "fieldname": "quantity_difference",
"in_list_view": 0, "fieldtype": "Read Only",
"in_standard_filter": 0, "label": "Quantity Difference"
"label": "Amount Difference", },
"length": 0, {
"no_copy": 0, "fieldname": "column_break_16",
"options": "Company:company:default_currency", "fieldtype": "Column Break"
"permlevel": 0, },
"precision": "", {
"print_hide": 0, "fieldname": "amount_difference",
"print_hide_if_no_value": 0, "fieldtype": "Currency",
"read_only": 1, "label": "Amount Difference",
"remember_last_selected_value": 0, "options": "Company:company:default_currency",
"report_hide": 0, "read_only": 1
"reqd": 0, },
"search_index": 0, {
"set_only_once": 0, "fieldname": "batch_no",
"unique": 0 "fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "modified": "2019-06-14 17:10:53.188305",
"hide_toolbar": 0, "modified_by": "Administrator",
"idx": 0, "module": "Stock",
"image_view": 0, "name": "Stock Reconciliation Item",
"in_create": 0, "owner": "Administrator",
"is_submittable": 0, "permissions": [],
"issingle": 0, "quick_entry": 1,
"istable": 1, "sort_field": "modified",
"max_attachments": 0, "sort_order": "DESC",
"menu_index": 0, "track_changes": 1
"modified": "2017-08-03 00:03:40.412071",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
} }

View File

@ -55,10 +55,12 @@ def get_conditions(filters):
#get all details #get all details
def get_stock_ledger_entries(filters): def get_stock_ledger_entries(filters):
conditions = get_conditions(filters) conditions = get_conditions(filters)
return frappe.db.sql("""select item_code, batch_no, warehouse, return frappe.db.sql("""
posting_date, actual_qty select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % where docstatus < 2 and ifnull(batch_no, '') != '' %s
group by voucher_no, batch_no, item_code, warehouse
order by item_code, warehouse""" %
conditions, as_dict=1) conditions, as_dict=1)
def get_item_warehouse_batch_map(filters, float_precision): def get_item_warehouse_batch_map(filters, float_precision):

View File

@ -157,9 +157,12 @@ class update_entries_after(object):
if sle.serial_no: if sle.serial_no:
self.get_serialized_values(sle) self.get_serialized_values(sle)
self.qty_after_transaction += flt(sle.actual_qty) self.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation":
self.qty_after_transaction = sle.qty_after_transaction
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
else: else:
if sle.voucher_type=="Stock Reconciliation": if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
# assert # assert
self.valuation_rate = sle.valuation_rate self.valuation_rate = sle.valuation_rate
self.qty_after_transaction = sle.qty_after_transaction self.qty_after_transaction = sle.qty_after_transaction
@ -371,7 +374,7 @@ class update_entries_after(object):
"""get Stock Ledger Entries after a particular datetime, for reposting""" """get Stock Ledger Entries after a particular datetime, for reposting"""
return get_stock_ledger_entries(self.previous_sle or frappe._dict({ return get_stock_ledger_entries(self.previous_sle or frappe._dict({
"item_code": self.args.get("item_code"), "warehouse": self.args.get("warehouse") }), "item_code": self.args.get("item_code"), "warehouse": self.args.get("warehouse") }),
">", "asc", for_update=True) ">", "asc", for_update=True, check_serial_no=False)
def raise_exceptions(self): def raise_exceptions(self):
deficiency = min(e["diff"] for e in self.exceptions) deficiency = min(e["diff"] for e in self.exceptions)
@ -412,7 +415,8 @@ def get_previous_sle(args, for_update=False):
sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
return sle and sle[0] or {} return sle and sle[0] or {}
def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=None, for_update=False, debug=False): def get_stock_ledger_entries(previous_sle, operator=None,
order="desc", limit=None, for_update=False, debug=False, check_serial_no=True):
"""get stock ledger entries filtered by specific posting datetime conditions""" """get stock ledger entries filtered by specific posting datetime conditions"""
conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator) conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator)
if previous_sle.get("warehouse"): if previous_sle.get("warehouse"):
@ -420,6 +424,9 @@ def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=No
elif previous_sle.get("warehouse_condition"): elif previous_sle.get("warehouse_condition"):
conditions += " and " + previous_sle.get("warehouse_condition") conditions += " and " + previous_sle.get("warehouse_condition")
if check_serial_no and previous_sle.get("serial_no"):
conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
if not previous_sle.get("posting_date"): if not previous_sle.get("posting_date"):
previous_sle["posting_date"] = "1900-01-01" previous_sle["posting_date"] = "1900-01-01"
if not previous_sle.get("posting_time"): if not previous_sle.get("posting_time"):
@ -479,6 +486,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \ if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \
and cint(erpnext.is_perpetual_inventory_enabled(company)): and cint(erpnext.is_perpetual_inventory_enabled(company)):
frappe.local.message_log = [] frappe.local.message_log = []
frappe.throw(_("Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting/cancelling this entry").format(item_code, voucher_type, voucher_no)) frappe.throw(_("Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting / cancelling this entry.")
.format(item_code, voucher_type, voucher_no))
return valuation_rate return valuation_rate

View File

@ -173,7 +173,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'), in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'),
args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'), args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'),
currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
raise_error_if_no_rate=True) raise_error_if_no_rate=raise_error_if_no_rate)
return in_rate return in_rate
@ -277,3 +277,7 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
new_row.append(None) new_row.append(None)
result[row_idx] = new_row result[row_idx] = new_row
def get_available_serial_nos(item_code, warehouse):
return frappe.get_all("Serial No", filters = {'item_code': item_code,
'warehouse': warehouse, 'delivery_document_no': ''}) or []

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More