diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py index f28a07431fe..88e1055beb4 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py @@ -27,4 +27,4 @@ def get_vouchar_detials(column_list, doctype, docname): for col in column_list: sanitize_searchfield(col) return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s''' - .format(columns=", ".join(json.loads(column_list)), doctype=doctype), docname, as_dict=1)[0] + .format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0] diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index 0b7cff3d63c..2235298201f 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -135,7 +135,7 @@ var create_import_button = function(frm) { callback: function(r) { if(!r.exc) { clearInterval(frm.page["interval"]); - frm.page.set_indicator(__('Import Successfull'), 'blue'); + frm.page.set_indicator(__('Import Successful'), 'blue'); create_reset_button(frm); } } diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py index c7604ec7ccd..58480df1190 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py @@ -13,7 +13,7 @@ def get_data(): }, { 'label': _('References'), - 'items': ['Period Closing Voucher', 'Request for Quotation', 'Tax Withholding Category'] + 'items': ['Period Closing Voucher', 'Tax Withholding Category'] }, { 'label': _('Target Details'), diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8680b710acf..ba68df7673f 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -21,7 +21,7 @@ from six import iteritems class POSInvoice(SalesInvoice): def __init__(self, *args, **kwargs): super(POSInvoice, self).__init__(*args, **kwargs) - + def validate(self): if not cint(self.is_pos): frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))) @@ -58,7 +58,7 @@ class POSInvoice(SalesInvoice): if self.redeem_loyalty_points and self.loyalty_points: self.apply_loyalty_points() self.set_status(update=True) - + def on_cancel(self): # run on cancel method of selling controller super(SalesInvoice, self).on_cancel() @@ -68,10 +68,10 @@ class POSInvoice(SalesInvoice): against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) against_psi_doc.delete_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry() - + def validate_stock_availablility(self): allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') - + for d in self.get('items'): if d.serial_no: filters = { @@ -89,11 +89,11 @@ class POSInvoice(SalesInvoice): for s in serial_nos: if s in reserved_serial_nos: invalid_serial_nos.append(s) - + if len(invalid_serial_nos): multiple_nos = 's' if len(invalid_serial_nos) > 1 else '' frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \ - Please select valid serial no.".format(d.idx, multiple_nos, + Please select valid serial no.".format(d.idx, multiple_nos, frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available")) else: if allow_negative_stock: @@ -105,9 +105,9 @@ class POSInvoice(SalesInvoice): .format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available")) elif flt(available_stock) < flt(d.qty): frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \ - Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), + Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available")) - + def validate_serialised_or_batched_item(self): for d in self.get("items"): serialized = d.get("has_serial_no") @@ -125,7 +125,7 @@ class POSInvoice(SalesInvoice): if batched and no_batch_selected: frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.' .format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) - + def validate_return_items(self): if not self.get("is_return"): return @@ -158,7 +158,7 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx)) if self.is_return and entry.amount > 0: frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx)) - + def validate_pos_return(self): if self.is_pos and self.is_return: total_amount_in_payments = 0 @@ -167,12 +167,12 @@ class POSInvoice(SalesInvoice): invoice_total = self.rounded_total or self.grand_total if total_amount_in_payments < invoice_total: frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total))) - + def validate_loyalty_transaction(self): if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"]) if not self.loyalty_redemption_account: - self.loyalty_redemption_account = expense_account + self.loyalty_redemption_account = expense_account if not self.loyalty_redemption_cost_center: self.loyalty_redemption_cost_center = cost_center @@ -212,7 +212,7 @@ class POSInvoice(SalesInvoice): if update: self.db_set('status', self.status, update_modified = update_modified) - + def set_pos_fields(self, for_validate=False): """Set retail related fields from POS Profiles""" from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile @@ -315,25 +315,25 @@ class POSInvoice(SalesInvoice): @frappe.whitelist() def get_stock_availability(item_code, warehouse): - latest_sle = frappe.db.sql("""select qty_after_transaction - from `tabStock Ledger Entry` + latest_sle = frappe.db.sql("""select qty_after_transaction + from `tabStock Ledger Entry` where item_code = %s and warehouse = %s order by posting_date desc, posting_time desc limit 1""", (item_code, warehouse), as_dict=1) - + pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item - where p.name = p_item.parent - and p.consolidated_invoice is NULL + where p.name = p_item.parent + and p.consolidated_invoice is NULL and p.docstatus = 1 and p_item.docstatus = 1 and p_item.item_code = %s and p_item.warehouse = %s """, (item_code, warehouse), as_dict=1) - + sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0 pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0 - + if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty: return sle_qty - pos_sales_qty else: @@ -360,14 +360,14 @@ def make_merge_log(invoices): merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log.posting_date = getdate(nowdate()) for inv in invoices: - inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), + inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), ["customer", "posting_date", "grand_total"], as_dict=1)[0] merge_log.customer = inv_data.customer merge_log.append("pos_invoices", { 'pos_invoice': inv.get('name'), 'customer': inv_data.customer, 'posting_date': inv_data.posting_date, - 'grand_total': inv_data.grand_total + 'grand_total': inv_data.grand_total }) if merge_log.get('pos_invoices'): diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 454c598d630..d4c17917899 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -302,10 +302,10 @@ "fieldname": "warehouse", "fieldtype": "Link", "label": "Warehouse", + "mandatory_depends_on": "update_stock", "oldfieldname": "warehouse", "oldfieldtype": "Link", - "options": "Warehouse", - "reqd": 1 + "options": "Warehouse" }, { "default": "0", @@ -350,4 +350,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3dab0540144..71f2e120ccf 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1619,22 +1619,23 @@ def update_multi_mode_option(doc, pos_profile): for pos_payment_method in pos_profile.get('payments'): pos_payment_method = pos_payment_method.as_dict() - + payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) - payment_mode[0].default = pos_payment_method.default - append_payment(payment_mode[0]) + if payment_mode: + payment_mode[0].default = pos_payment_method.default + append_payment(payment_mode[0]) def get_all_mode_of_payments(doc): return frappe.db.sql(""" - select mpa.default_account, mpa.parent, mp.type as type - from `tabMode of Payment Account` mpa,`tabMode of Payment` mp + select mpa.default_account, mpa.parent, mp.type as type + from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", {'company': doc.company}, as_dict=1) def get_mode_of_payment_info(mode_of_payment, company): return frappe.db.sql(""" - select mpa.default_account, mpa.parent, mp.type as type - from `tabMode of Payment Account` mpa,`tabMode of Payment` mp + select mpa.default_account, mpa.parent, mp.type as type + from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", (company, mode_of_payment), as_dict=1) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 964566a17ef..9660c9570e2 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -206,10 +206,19 @@ class TestSalesInvoice(unittest.TestCase): "rate": 14, 'included_in_print_rate': 1 }) + si.append("taxes", { + "charge_type": "On Item Quantity", + "account_head": "_Test Account Education Cess - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "CESS", + "rate": 5, + 'included_in_print_rate': 1 + }) si.insert() # with inclusive tax - self.assertEqual(si.net_total, 4385.96) + self.assertEqual(si.items[0].net_amount, 3947.368421052631) + self.assertEqual(si.net_total, 3947.37) self.assertEqual(si.grand_total, 5000) si.reload() @@ -222,8 +231,8 @@ class TestSalesInvoice(unittest.TestCase): si.save() # with inclusive tax and additional discount - self.assertEqual(si.net_total, 4285.96) - self.assertEqual(si.grand_total, 4885.99) + self.assertEqual(si.net_total, 3847.37) + self.assertEqual(si.grand_total, 4886) si.reload() @@ -235,7 +244,7 @@ class TestSalesInvoice(unittest.TestCase): si.save() # with inclusive tax and additional discount - self.assertEqual(si.net_total, 4298.25) + self.assertEqual(si.net_total, 3859.65) self.assertEqual(si.grand_total, 4900.00) def test_sales_invoice_discount_amount(self): diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 3785ebf215f..d5b8cdb1d4d 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -14,7 +14,7 @@ import frappe, erpnext from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency from erpnext.accounts.utils import get_fiscal_year from frappe import _ -from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr) +from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr, cint) from six import itervalues from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children @@ -46,7 +46,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ start_date = year_start_date months = get_months(year_start_date, year_end_date) - for i in range(math.ceil(months / months_to_add)): + for i in range(cint(math.ceil(months / months_to_add))): period = frappe._dict({ "from_date": start_date }) diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py index 148357f3921..34facd8d050 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py @@ -11,7 +11,7 @@ from erpnext.assets.doctype.asset_maintenance.asset_maintenance import calculate class AssetMaintenanceLog(Document): def validate(self): - if getdate(self.due_date) < getdate(nowdate()): + if getdate(self.due_date) < getdate(nowdate()) and self.maintenance_status not in ["Completed", "Cancelled"]: self.maintenance_status = "Overdue" if self.maintenance_status == "Completed" and not self.completion_date: diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js index b854413310a..23000e60eff 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js @@ -1,14 +1,15 @@ frappe.listview_settings['Asset Maintenance Log'] = { add_fields: ["maintenance_status"], + has_indicator_for_draft: 1, get_indicator: function(doc) { - if(doc.maintenance_status=="Pending") { - return [__("Pending"), "orange"]; - } else if(doc.maintenance_status=="Completed") { - return [__("Completed"), "green"]; - } else if(doc.maintenance_status=="Cancelled") { - return [__("Cancelled"), "red"]; - } else if(doc.maintenance_status=="Overdue") { - return [__("Overdue"), "red"]; + if (doc.maintenance_status=="Planned") { + return [__(doc.maintenance_status), "orange", "status,=," + doc.maintenance_status]; + } else if (doc.maintenance_status=="Completed") { + return [__(doc.maintenance_status), "green", "status,=," + doc.maintenance_status]; + } else if (doc.maintenance_status=="Cancelled") { + return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; + } else if (doc.maintenance_status=="Overdue") { + return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; } } }; diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 25065ab155f..9f2b9714f74 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -94,7 +94,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) { this.frm.add_custom_button(__('Update Items'), () => { erpnext.utils.update_child_items({ - frm: frm, + frm: this.frm, child_docname: "items", child_doctype: "Purchase Order Detail", cannot_add_row: false, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 66b5f3035db..3091193b8dc 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -985,7 +985,7 @@ def validate_inclusive_tax(tax, doc): # all rows about the reffered tax should be inclusive _on_previous_row_error("1 - %d" % (tax.row_id,)) elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not marked as Inclusive")) + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 37b7e31e611..c88bf66411f 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -497,24 +497,18 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): conditions, bin_conditions = [], [] filter_dict = get_doctype_wise_filters(filters) - sub_query = """ select round(`tabBin`.actual_qty, 2) from `tabBin` - where `tabBin`.warehouse = `tabWarehouse`.name - {bin_conditions} """.format( - bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"), - bin_conditions, ignore_permissions=True)) - query = """select `tabWarehouse`.name, - CONCAT_WS(" : ", "Actual Qty", ifnull( ({sub_query}), 0) ) as actual_qty - from `tabWarehouse` + CONCAT_WS(" : ", "Actual Qty", ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty + from `tabWarehouse` left join `tabBin` + on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions} where - `tabWarehouse`.`{key}` like {txt} + `tabWarehouse`.`{key}` like {txt} {fcond} {mcond} - order by - `tabWarehouse`.name desc + order by ifnull(`tabBin`.actual_qty, 0) desc limit {start}, {page_len} """.format( - sub_query=sub_query, + bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"),bin_conditions, ignore_permissions=True), key=searchfield, fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions), mcond=get_match_cond(doctype), diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 572e1ca2397..2a14be85325 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -161,8 +161,9 @@ class calculate_taxes_and_totals(object): for item in self.doc.get("items"): item_tax_map = self._load_item_tax_rate(item.item_tax_rate) cumulated_tax_fraction = 0 + total_inclusive_tax_amount_per_qty = 0 for i, tax in enumerate(self.doc.get("taxes")): - tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax, item_tax_map) + tax.tax_fraction_for_current_item, inclusive_tax_amount_per_qty = self.get_current_tax_fraction(tax, item_tax_map) if i==0: tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item @@ -172,9 +173,12 @@ class calculate_taxes_and_totals(object): + tax.tax_fraction_for_current_item cumulated_tax_fraction += tax.tax_fraction_for_current_item + total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.qty) - if cumulated_tax_fraction and not self.discount_amount_applied and item.qty: - item.net_amount = flt(item.amount / (1 + cumulated_tax_fraction)) + if not self.discount_amount_applied and item.qty and (cumulated_tax_fraction or total_inclusive_tax_amount_per_qty): + amount = flt(item.amount) - total_inclusive_tax_amount_per_qty + + item.net_amount = flt(amount / (1 + cumulated_tax_fraction)) item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate")) item.discount_percentage = flt(item.discount_percentage, item.precision("discount_percentage")) @@ -190,6 +194,7 @@ class calculate_taxes_and_totals(object): from tax inclusive amount """ current_tax_fraction = 0 + inclusive_tax_amount_per_qty = 0 if cint(tax.included_in_print_rate): tax_rate = self._get_tax_rate(tax, item_tax_map) @@ -204,10 +209,15 @@ class calculate_taxes_and_totals(object): elif tax.charge_type == "On Previous Row Total": current_tax_fraction = (tax_rate / 100.0) * \ self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item + + elif tax.charge_type == "On Item Quantity": + inclusive_tax_amount_per_qty = flt(tax_rate) - if getattr(tax, "add_deduct_tax", None): - current_tax_fraction *= -1.0 if (tax.add_deduct_tax == "Deduct") else 1.0 - return current_tax_fraction + if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct": + current_tax_fraction *= -1.0 + inclusive_tax_amount_per_qty *= -1.0 + + return current_tax_fraction, inclusive_tax_amount_per_qty def _get_tax_rate(self, tax, item_tax_map): if tax.account_head in item_tax_map: @@ -321,7 +331,7 @@ class calculate_taxes_and_totals(object): current_tax_amount = (tax_rate / 100.0) * \ self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_for_current_item elif tax.charge_type == "On Item Quantity": - current_tax_amount = tax_rate * item.stock_qty + current_tax_amount = tax_rate * item.qty self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) @@ -472,7 +482,7 @@ class calculate_taxes_and_totals(object): actual_taxes_dict = {} for tax in self.doc.get("taxes"): - if tax.charge_type == "Actual": + if tax.charge_type in ["Actual", "On Item Quantity"]: tax_amount = self.get_tax_amount_if_for_valuation_or_deduction(tax.tax_amount, tax) actual_taxes_dict.setdefault(tax.idx, tax_amount) elif tax.row_id in actual_taxes_dict: diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 5cd5233b2e4..b61cad36209 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -411,7 +411,7 @@ "fieldname": "lost_reasons", "fieldtype": "Table MultiSelect", "label": "Lost Reasons", - "options": "Lost Reason Detail", + "options": "Opportunity Lost Reason Detail", "read_only": 1 }, { diff --git a/erpnext/crm/doctype/opportunity_lost_reason_detail/__init__.py b/erpnext/crm/doctype/opportunity_lost_reason_detail/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.json b/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.json new file mode 100644 index 00000000000..50620e2c347 --- /dev/null +++ b/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2020-07-16 16:11:39.830389", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "lost_reason" + ], + "fields": [ + { + "fieldname": "lost_reason", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Opportunity Lost Reason", + "options": "Opportunity Lost Reason" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-26 17:58:26.313242", + "modified_by": "Administrator", + "module": "CRM", + "name": "Opportunity Lost Reason Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.py b/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.py new file mode 100644 index 00000000000..8723f1d0457 --- /dev/null +++ b/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 OpportunityLostReasonDetail(Document): + pass diff --git a/erpnext/hr/doctype/department/department.json b/erpnext/hr/doctype/department/department.json index a54c1d18e7c..dcb6a742b75 100644 --- a/erpnext/hr/doctype/department/department.json +++ b/erpnext/hr/doctype/department/department.json @@ -17,10 +17,10 @@ "payroll_cost_center", "column_break_9", "leave_block_list", - "leave_section", + "approvers", "leave_approvers", - "expense_section", "expense_approvers", + "shift_request_approver", "lft", "rgt", "old_parent" @@ -33,14 +33,18 @@ "label": "Department", "oldfieldname": "department_name", "oldfieldtype": "Data", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "parent_department", "fieldtype": "Link", "in_list_view": 1, "label": "Parent Department", - "options": "Department" + "options": "Department", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -48,7 +52,9 @@ "in_standard_filter": 1, "label": "Company", "options": "Company", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -56,17 +62,23 @@ "fieldname": "is_group", "fieldtype": "Check", "in_list_view": 1, - "label": "Is Group" + "label": "Is Group", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "label": "Disabled" + "label": "Disabled", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_4", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "description": "Days for which Holidays are blocked for this department.", @@ -74,31 +86,25 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Leave Block List", - "options": "Leave Block List" + "options": "Leave Block List", + "show_days": 1, + "show_seconds": 1 }, { - "fieldname": "leave_section", - "fieldtype": "Section Break", - "label": "Leave Approvers" - }, - { - "description": "The first Leave Approver in the list will be set as the default Leave Approver.", "fieldname": "leave_approvers", "fieldtype": "Table", "label": "Leave Approver", - "options": "Department Approver" + "options": "Department Approver", + "show_days": 1, + "show_seconds": 1 }, { - "fieldname": "expense_section", - "fieldtype": "Section Break", - "label": "Expense Approvers" - }, - { - "description": "The first Expense Approver in the list will be set as the default Expense Approver.", "fieldname": "expense_approvers", "fieldtype": "Table", "label": "Expense Approver", - "options": "Department Approver" + "options": "Department Approver", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "lft", @@ -106,7 +112,9 @@ "hidden": 1, "label": "lft", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rgt", @@ -114,7 +122,9 @@ "hidden": 1, "label": "rgt", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "old_parent", @@ -122,28 +132,52 @@ "hidden": 1, "ignore_user_permissions": 1, "label": "Old Parent", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_3", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payroll_cost_center", "fieldtype": "Link", "label": "Payroll Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_9", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "description": "The first Approver in the list will be set as the default Approver.", + "fieldname": "approvers", + "fieldtype": "Section Break", + "label": "Approvers", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "shift_request_approver", + "fieldtype": "Table", + "label": "Shift Request Approver", + "options": "Department Approver", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-sitemap", "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-05-05 18:49:28.503931", + "modified": "2020-06-23 15:42:00.563272", "modified_by": "Administrator", "module": "HR", "name": "Department", diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index afd54b83469..9b2de0e1cbc 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -15,12 +15,12 @@ class DepartmentApprover(Document): def get_approvers(doctype, txt, searchfield, start, page_len, filters): if not filters.get("employee"): - frappe.throw(_("Please select Employee Record first.")) + frappe.throw(_("Please select Employee first.")) approvers = [] department_details = {} department_list = [] - employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver"], as_dict=True) + employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True) employee_department = filters.get("department") or employee.department if employee_department: @@ -37,13 +37,18 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): if filters.get("doctype") == "Expense Claim" and employee.expense_approver: approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name'])) + if filters.get("doctype") == "Shift Request" and employee.shift_request_approver: + approvers.append(frappe.db.get_value("User", employee.shift_request_approver, ['name', 'first_name', 'last_name'])) if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" field_name = "Leave Approver" - else: + elif filters.get("doctype") == "Expense Claim": parentfield = "expense_approvers" field_name = "Expense Approver" + elif filters.get("doctype") == "Shift Request": + parentfield = "shift_request_approver" + field_name = "Shift Request Approver" if department_list: for d in department_list: approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index f2afe065d1e..8c02e4f1d64 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -51,10 +51,14 @@ "column_break_31", "grade", "branch", + "approvers_section", + "expense_approver", + "leave_approver", + "column_break_45", + "shift_request_approver", "attendance_and_leave_details", "leave_policy", "attendance_device_id", - "leave_approver", "column_break_44", "holiday_list", "default_shift", @@ -62,7 +66,6 @@ "salary_mode", "payroll_cost_center", "column_break_52", - "expense_approver", "bank_name", "bank_ac_no", "health_insurance_section", @@ -806,14 +809,37 @@ "fieldname": "expense_approver", "fieldtype": "Link", "label": "Expense Approver", - "options": "User" + "options": "User", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "approvers_section", + "fieldtype": "Section Break", + "label": "Approvers", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_45", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "shift_request_approver", + "fieldtype": "Link", + "label": "Shift Request Approver", + "options": "User", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2020-07-03 21:28:04.109189", + "modified": "2020-07-28 01:36:04.109189", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim_list.js b/erpnext/hr/doctype/expense_claim/expense_claim_list.js index 6195ad414a1..9bafc185628 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim_list.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim_list.js @@ -1,5 +1,5 @@ frappe.listview_settings['Expense Claim'] = { - add_fields: ["total_claimed_amount", "docstatus"], + add_fields: ["total_claimed_amount", "docstatus", "company"], get_indicator: function(doc) { if(doc.status == "Paid") { return [__("Paid"), "green", "status,=,Paid"]; diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index e7e1a37480b..3d68bc8d8e0 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -24,8 +24,13 @@ class JobOffer(Document): check_vacancies = frappe.get_single("HR Settings").check_vacancies if staffing_plan and check_vacancies: job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) - if staffing_plan.vacancies - len(job_offers) <= 0: - frappe.throw(_("There are no vacancies under staffing plan {0}").format(frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)))) + + if not staffing_plan.get("vacancies") or staffing_plan.vacancies - len(job_offers) <= 0: + error_variable = 'for ' + frappe.bold(self.designation) + if staffing_plan.get("parent"): + error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)) + + frappe.throw(_("There are no vacancies under staffing plan {0}").format(error_variable)) def on_change(self): update_job_applicant(self.status, self.job_applicant) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js index 210a73cfe55..e9e129cdd24 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js @@ -5,20 +5,23 @@ cur_frm.add_fetch('employee','employee_name','employee_name'); frappe.ui.form.on("Leave Allocation", { onload: function(frm) { + // Ignore cancellation of doctype on cancel all. + frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; + if(!frm.doc.from_date) frm.set_value("from_date", frappe.datetime.get_today()); frm.set_query("employee", function() { return { query: "erpnext.controllers.queries.employee_query" - } + }; }); frm.set_query("leave_type", function() { return { filters: { is_lwp: 0 } - } - }) + }; + }); }, refresh: function(frm) { diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 4001a455075..d62e418b17e 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -19,6 +19,10 @@ frappe.ui.form.on("Leave Application", { frm.set_query("employee", erpnext.queries.employee); }, onload: function(frm) { + + // Ignore cancellation of doctype on cancel all. + frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; + if (!frm.doc.posting_date) { frm.set_value("posting_date", frappe.datetime.get_today()); } diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.js b/erpnext/hr/doctype/leave_encashment/leave_encashment.js index 701c2f0f31f..71a34226da4 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.js +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.js @@ -2,6 +2,10 @@ // For license information, please see license.txt frappe.ui.form.on('Leave Encashment', { + onload: function(frm) { + // Ignore cancellation of doctype on cancel all. + frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; + }, setup: function(frm) { frm.set_query("leave_type", function() { return { @@ -33,7 +37,7 @@ frappe.ui.form.on('Leave Encashment', { doc: frm.doc, callback: function(r) { frm.refresh_fields(); - } + } }); } } diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.json b/erpnext/hr/doctype/shift_assignment/shift_assignment.json index 72cbba8a0dc..ce2a10f229f 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.json +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.json @@ -10,9 +10,11 @@ "employee", "employee_name", "shift_type", + "status", "column_break_3", "company", - "date", + "start_date", + "end_date", "shift_request", "department", "amended_from" @@ -59,12 +61,6 @@ "options": "Company", "reqd": 1 }, - { - "fieldname": "date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "Date" - }, { "fieldname": "shift_request", "fieldtype": "Link", @@ -80,11 +76,36 @@ "options": "Shift Assignment", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Start Date", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "show_days": 1, + "show_seconds": 1 + }, + { + "allow_on_submit": 1, + "default": "Active", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Active\nInactive", + "show_days": 1, + "show_seconds": 1 } ], "is_submittable": 1, "links": [], - "modified": "2019-12-12 15:49:06.956901", + "modified": "2020-06-15 14:27:54.310773", "modified_by": "Administrator", "module": "HR", "name": "Shift Assignment", diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 40c78cdf072..f8b73349c19 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -11,38 +11,63 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from datetime import timedelta, datetime -class OverlapError(frappe.ValidationError): pass - class ShiftAssignment(Document): def validate(self): self.validate_overlapping_dates() + if self.end_date and self.end_date <= self.start_date: + frappe.throw(_("End Date must not be lesser than Start Date")) + def validate_overlapping_dates(self): - if not self.name: - self.name = "New Shift Assignment" + if not self.name: + self.name = "New Shift Assignment" - d = frappe.db.sql(""" - select - name, shift_type, date - from `tabShift Assignment` - where employee = %(employee)s and docstatus < 2 - and date = %(date)s - and name != %(name)s""", { - "employee": self.employee, - "shift_type": self.shift_type, - "date": self.date, - "name": self.name - }, as_dict = 1) + condition = """and ( + end_date is null + or + %(start_date)s between start_date and end_date + """ - for date_overlap in d: - if date_overlap['name']: - self.throw_overlap_error(date_overlap) + if self.end_date: + condition += """ or + %(end_date)s between start_date and end_date + or + start_date between %(start_date)s and %(end_date)s + ) """ + else: + condition += """ ) """ - def throw_overlap_error(self, d): - msg = _("Employee {0} has already applied for {1} on {2} : ").format(self.employee, - d['shift_type'], formatdate(d['date'])) \ - + """ {0}""".format(d["name"]) - frappe.throw(msg, OverlapError) + assigned_shifts = frappe.db.sql(""" + select name, shift_type, start_date ,end_date, docstatus, status + from `tabShift Assignment` + where + employee=%(employee)s and docstatus = 1 + and name != %(name)s + and status = "Active" + {0} + """.format(condition), { + "employee": self.employee, + "shift_type": self.shift_type, + "start_date": self.start_date, + "end_date": self.end_date, + "name": self.name + }, as_dict = 1) + + if len(assigned_shifts): + self.throw_overlap_error(assigned_shifts[0]) + + def throw_overlap_error(self, shift_details): + shift_details = frappe._dict(shift_details) + if shift_details.docstatus == 1 and shift_details.status == "Active": + msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name)) + if shift_details.start_date: + msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y")) + title = "Ongoing Shift" + if shift_details.end_date: + msg += _(" to {0}").format(getdate(self.end_date).strftime("%d-%m-%Y")) + title = "Active Shift" + if msg: + frappe.throw(msg, title=title) @frappe.whitelist() def get_events(start, end, filters=None): @@ -62,19 +87,22 @@ def get_events(start, end, filters=None): return events def add_assignments(events, start, end, conditions=None): - query = """select name, date, employee_name, + query = """select name, start_date, end_date, employee_name, employee, docstatus from `tabShift Assignment` where - date <= %(date)s - and docstatus < 2""" + start_date >= %(start_date)s + or end_date <= %(end_date)s + or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date) + and docstatus = 1""" if conditions: query += conditions - for d in frappe.db.sql(query, {"date":start, "date":end}, as_dict=True): + for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True): e = { "name": d.name, "doctype": "Shift Assignment", - "date": d.date, + "start_date": d.start_date, + "end_date": d.end_date if d.end_date else nowdate(), "title": cstr(d.employee_name) + \ cstr(d.shift_type), "docstatus": d.docstatus @@ -92,7 +120,16 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals :param next_shift_direction: One of: None, 'forward', 'reverse'. Direction to look for next shift if shift not found on given date. """ default_shift = frappe.db.get_value('Employee', employee, 'default_shift') - shift_type_name = frappe.db.get_value('Shift Assignment', {'employee':employee, 'date': for_date, 'docstatus': '1'}, 'shift_type') + shift_type_name = None + shift_assignment_details = frappe.db.get_value('Shift Assignment', {'employee':employee, 'start_date':('<=', for_date), 'docstatus': '1', 'status': "Active"}, ['shift_type', 'end_date']) + + if shift_assignment_details: + shift_type_name = shift_assignment_details[0] + + # if end_date present means that shift is over after end_date else it is a ongoing shift. + if shift_assignment_details[1] and for_date >= shift_assignment_details[1] : + shift_type_name = None + if not shift_type_name and consider_default_shift: shift_type_name = default_shift if shift_type_name: @@ -117,16 +154,20 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals direction = '<' if next_shift_direction == 'reverse' else '>' sort_order = 'desc' if next_shift_direction == 'reverse' else 'asc' dates = frappe.db.get_all('Shift Assignment', - 'date', - {'employee':employee, 'date':(direction, for_date), 'docstatus': '1'}, + ['start_date', 'end_date'], + {'employee':employee, 'start_date':(direction, for_date), 'docstatus': '1', "status": "Active"}, as_list=True, - limit=MAX_DAYS, order_by="date "+sort_order) - for date in dates: - shift_details = get_employee_shift(employee, date[0], consider_default_shift, None) - if shift_details: - shift_type_name = shift_details.shift_type.name - for_date = date[0] - break + limit=MAX_DAYS, order_by="start_date "+sort_order) + + if dates: + for date in dates: + if date[1] and date[1] < for_date: + continue + shift_details = get_employee_shift(employee, date[0], consider_default_shift, None) + if shift_details: + shift_type_name = shift_details.shift_type.name + for_date = date[0] + break return get_shift_details(shift_type_name, for_date) @@ -134,7 +175,7 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals def get_employee_shift_timings(employee, for_timestamp=now_datetime(), consider_default_shift=False): """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee """ - # write and verify a test case for midnight shift. + # write and verify a test case for midnight shift. prev_shift = curr_shift = next_shift = None curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward') if curr_shift: diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js index c2c9bc073ad..17a986deb21 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js @@ -3,8 +3,8 @@ frappe.views.calendar["Shift Assignment"] = { field_map: { - "start": "date", - "end": "date", + "start": "start_date", + "end": "end_date", "id": "name", "docstatus": 1 }, diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py index 7fe80a236c6..4c3c1ed579e 100644 --- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import nowdate +from frappe.utils import nowdate, add_days test_dependencies = ["Shift Type"] @@ -20,8 +20,61 @@ class TestShiftAssignment(unittest.TestCase): "shift_type": "Day Shift", "company": "_Test Company", "employee": "_T-Employee-00001", - "date": nowdate() + "start_date": nowdate() }).insert() shift_assignment.submit() self.assertEqual(shift_assignment.docstatus, 1) + + def test_overlapping_for_ongoing_shift(self): + # shift should be Ongoing if Only start_date is present and status = Active + + shift_assignment_1 = frappe.get_doc({ + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": nowdate(), + "status": 'Active' + }).insert() + shift_assignment_1.submit() + + self.assertEqual(shift_assignment_1.docstatus, 1) + + shift_assignment = frappe.get_doc({ + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": add_days(nowdate(), 2) + }) + + self.assertRaises(frappe.ValidationError, shift_assignment.save) + + def test_overlapping_for_fixed_period_shift(self): + # shift should is for Fixed period if Only start_date and end_date both are present and status = Active + + shift_assignment_1 = frappe.get_doc({ + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": nowdate(), + "end_date": add_days(nowdate(), 30), + "status": 'Active' + }).insert() + shift_assignment_1.submit() + + + # it should not allowed within period of any shift. + shift_assignment_3 = frappe.get_doc({ + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date":add_days(nowdate(), 10), + "end_date": add_days(nowdate(), 35), + "status": 'Active' + }) + + self.assertRaises(frappe.ValidationError, shift_assignment_3.save) \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_request/shift_request.js b/erpnext/hr/doctype/shift_request/shift_request.js index 1db7c7d10e5..b17a6f3845b 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.js +++ b/erpnext/hr/doctype/shift_request/shift_request.js @@ -2,7 +2,16 @@ // For license information, please see license.txt frappe.ui.form.on('Shift Request', { - refresh: function(frm) { - - } + setup: function(frm) { + frm.set_query("approver", function() { + return { + query: "erpnext.hr.doctype.department_approver.department_approver.get_approvers", + filters: { + employee: frm.doc.employee, + doctype: frm.doc.doctype + } + }; + }); + frm.set_query("employee", erpnext.queries.employee); + }, }); diff --git a/erpnext/hr/doctype/shift_request/shift_request.json b/erpnext/hr/doctype/shift_request/shift_request.json index dd056470cd3..64cbdfff7da 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.json +++ b/erpnext/hr/doctype/shift_request/shift_request.json @@ -1,396 +1,155 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "HR-SHR-.YY.-.MM.-.#####", - "beta": 0, - "creation": "2018-04-13 16:32:27.974273", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "HR-SHR-.YY.-.MM.-.#####", + "creation": "2018-04-13 16:32:27.974273", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "shift_type", + "employee", + "employee_name", + "department", + "status", + "column_break_4", + "company", + "approver", + "from_date", + "to_date", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shift_type", - "fieldtype": "Link", - "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": "Shift Type", - "length": 0, - "no_copy": 0, - "options": "Shift Type", - "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 - }, + "fieldname": "shift_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Shift Type", + "options": "Shift Type", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "fieldtype": "Link", - "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": "Employee", - "length": 0, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "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": "Employee 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 - }, + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "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": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "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 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "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 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "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": 1, - "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 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "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": "From 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 - }, + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "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": "To 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 - }, + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Shift Request", - "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 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Shift Request", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Draft\nApproved\nRejected", + "reqd": 1 + }, + { + "fetch_from": "employee.shift_request_approver", + "fetch_if_empty": 1, + "fieldname": "approver", + "fieldtype": "Link", + "label": "Approver", + "options": "User", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:36.577448", - "modified_by": "Administrator", - "module": "HR", - "name": "Shift Request", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-08-10 17:59:31.550558", + "modified_by": "Administrator", + "module": "HR", + "name": "Shift Request", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "employee_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "employee_name", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py index ff5de08ee5f..1c2801bf08f 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.py +++ b/erpnext/hr/doctype/shift_request/shift_request.py @@ -14,19 +14,26 @@ class ShiftRequest(Document): def validate(self): self.validate_dates() self.validate_shift_request_overlap_dates() + self.validate_approver() + self.validate_default_shift() def on_submit(self): - date_list = self.get_working_days(self.from_date, self.to_date) - for date in date_list: + if self.status not in ["Approved", "Rejected"]: + frappe.throw(_("Only Shift Request with status 'Approved' and 'Rejected' can be submitted")) + if self.status == "Approved": assignment_doc = frappe.new_doc("Shift Assignment") assignment_doc.company = self.company assignment_doc.shift_type = self.shift_type assignment_doc.employee = self.employee - assignment_doc.date = date + assignment_doc.start_date = self.from_date + if self.to_date: + assignment_doc.end_date = self.to_date assignment_doc.shift_request = self.name assignment_doc.insert() assignment_doc.submit() + frappe.msgprint(_("Shift Assignment: {0} created for Employee: {1}").format(frappe.bold(assignment_doc.name), frappe.bold(self.employee))) + def on_cancel(self): shift_assignment_list = frappe.get_list("Shift Assignment", {'employee': self.employee, 'shift_request': self.name}) if shift_assignment_list: @@ -34,6 +41,19 @@ class ShiftRequest(Document): shift_assignment_doc = frappe.get_doc("Shift Assignment", shift['name']) shift_assignment_doc.cancel() + def validate_default_shift(self): + default_shift = frappe.get_value("Employee", self.employee, "default_shift") + if self.shift_type == default_shift: + frappe.throw(_("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type))) + + def validate_approver(self): + department = frappe.get_value("Employee", self.employee, "department") + shift_approver = frappe.get_value("Employee", self.employee, "shift_request_approver") + approvers = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department)) + approvers = [approver[0] for approver in approvers] + approvers.append(shift_approver) + if self.approver not in approvers: + frappe.throw(_("Only Approvers can Approve this Request.")) def validate_dates(self): if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)): @@ -68,28 +88,4 @@ class ShiftRequest(Document): msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee, d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \ + """ {0}""".format(d["name"]) - frappe.throw(msg, OverlapError) - - def get_working_days(self, start_date, end_date): - start_date, end_date = getdate(start_date), getdate(end_date) - - from datetime import timedelta - - date_list = [] - employee_holiday_list = [] - - employee_holidays = frappe.db.sql("""select holiday_date from `tabHoliday` - where parent in (select holiday_list from `tabEmployee` - where name = %s)""",self.employee,as_dict=1) - - for d in employee_holidays: - employee_holiday_list.append(d.holiday_date) - - reference_date = start_date - - while reference_date <= end_date: - if reference_date not in employee_holiday_list: - date_list.append(reference_date) - reference_date += timedelta(days=1) - - return date_list \ No newline at end of file + frappe.throw(msg, OverlapError) \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py index 1d0cf719c29..3dcfcbf4a5b 100644 --- a/erpnext/hr/doctype/shift_request/test_shift_request.py +++ b/erpnext/hr/doctype/shift_request/test_shift_request.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import nowdate +from frappe.utils import nowdate, add_days class TestShiftRequest(unittest.TestCase): def setUp(self): @@ -13,14 +13,20 @@ class TestShiftRequest(unittest.TestCase): frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) def test_make_shift_request(self): + department = frappe.get_value("Employee", "_T-Employee-00001", 'department') + set_shift_approver(department) + approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] + shift_request = frappe.get_doc({ "doctype": "Shift Request", "shift_type": "Day Shift", "company": "_Test Company", "employee": "_T-Employee-00001", "employee_name": "_Test Employee", - "start_date": nowdate(), - "end_date": nowdate() + "from_date": nowdate(), + "to_date": add_days(nowdate(), 10), + "approver": approver, + "status": "Approved" }) shift_request.insert() shift_request.submit() @@ -34,4 +40,10 @@ class TestShiftRequest(unittest.TestCase): self.assertEqual(shift_request.employee, employee) shift_request.cancel() shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')}) - self.assertEqual(shift_assignment_doc.docstatus, 2) \ No newline at end of file + self.assertEqual(shift_assignment_doc.docstatus, 2) + +def set_shift_approver(department): + department_doc = frappe.get_doc("Department", department) + department_doc.append('shift_request_approver',{'approver': "test1@example.com"}) + department_doc.save() + department_doc.reload() \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_type/shift_type.js b/erpnext/hr/doctype/shift_type/shift_type.js index e6335456303..ba53312bcef 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.js +++ b/erpnext/hr/doctype/shift_type/shift_type.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Shift Type', { refresh: function(frm) { frm.add_custom_button( - 'Mark Auto Attendance', + 'Mark Attendance', () => frm.call({ doc: frm.doc, method: 'process_auto_attendance', diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index 19735648aa9..054e7e3688e 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -79,9 +79,10 @@ class ShiftType(Document): mark_attendance(employee, date, 'Absent', self.name) def get_assigned_employee(self, from_date=None, consider_default_shift=False): - filters = {'date':('>=', from_date), 'shift_type': self.name, 'docstatus': '1'} + filters = {'start_date':('>', from_date), 'shift_type': self.name, 'docstatus': '1'} if not from_date: - del filters['date'] + del filters["start_date"] + assigned_employees = frappe.get_all('Shift Assignment', 'employee', filters, as_list=True) assigned_employees = [x[0] for x in assigned_employees] diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index f051755f67c..bac6e638d74 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -16,14 +16,16 @@ from six import string_types class LoanApplication(Document): def validate(self): - - validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, - self.repayment_periods, self.is_term_loan) - - self.validate_loan_type() self.set_pledge_amount() self.set_loan_amount() self.validate_loan_amount() + + if self.is_term_loan: + validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, + self.repayment_periods, self.is_term_loan) + + self.validate_loan_type() + self.get_repayment_details() self.check_sanctioned_amount_limit() @@ -106,7 +108,7 @@ class LoanApplication(Document): if self.is_secured_loan and self.proposed_pledges: self.maximum_loan_amount = 0 for security in self.proposed_pledges: - self.maximum_loan_amount += security.post_haircut_amount + self.maximum_loan_amount += flt(security.post_haircut_amount) if not self.loan_amount and self.is_secured_loan and self.proposed_pledges: self.loan_amount = self.maximum_loan_amount @@ -133,10 +135,7 @@ def create_loan(source_name, target_doc=None, submit=0): "validation": { "docstatus": ["=", 1] }, - "postprocess": update_accounts, - "field_no_map": [ - "is_secured_loan" - ] + "postprocess": update_accounts } }, target_doc) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index d44088bee74..6c27e121347 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -10,22 +10,20 @@ from frappe.utils import nowdate, getdate, add_days, flt from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans +from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty +from frappe.utils import get_datetime class LoanDisbursement(AccountsController): def validate(self): self.set_missing_values() - def before_submit(self): - self.set_status_and_amounts() - - def before_cancel(self): - self.set_status_and_amounts(cancel=1) - def on_submit(self): + self.set_status_and_amounts() self.make_gl_entries() def on_cancel(self): + self.set_status_and_amounts(cancel=1) self.make_gl_entries(cancel=1) self.ignore_linked_doctypes = ['GL Entry'] @@ -45,29 +43,69 @@ class LoanDisbursement(AccountsController): def set_status_and_amounts(self, cancel=0): loan_details = frappe.get_all("Loan", - fields = ["loan_amount", "disbursed_amount", "total_principal_paid", "status", "is_term_loan"], - filters= { "name": self.against_loan } - )[0] - - if loan_details.status == "Disbursed" and not loan_details.is_term_loan: - process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), - loan=self.against_loan) + fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable", + "status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0] if cancel: disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount + total_payment = loan_details.total_payment + + if loan_details.disbursed_amount > loan_details.loan_amount: + topup_amount = loan_details.disbursed_amount - loan_details.loan_amount + if topup_amount > self.disbursed_amount: + topup_amount = self.disbursed_amount + + total_payment = total_payment - topup_amount + if disbursed_amount == 0: status = "Sanctioned" - elif disbursed_amount >= loan_details.disbursed_amount: + elif disbursed_amount >= loan_details.loan_amount: status = "Disbursed" else: status = "Partially Disbursed" else: disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount + total_payment = loan_details.total_payment - if flt(disbursed_amount) - flt(loan_details.total_principal_paid) > flt(loan_details.loan_amount): + if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan: frappe.throw(_("Disbursed Amount cannot be greater than loan amount")) - if flt(disbursed_amount) >= loan_details.disbursed_amount: + if loan_details.status == 'Disbursed': + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) + else: + pending_principal_amount = loan_details.disbursed_amount + + security_value = 0.0 + if loan_details.is_secured_loan: + security_value = get_total_pledged_security_value(self.against_loan) + + if not security_value: + security_value = loan_details.loan_amount + + if pending_principal_amount + self.disbursed_amount > flt(security_value): + allowed_amount = security_value - pending_principal_amount + if allowed_amount < 0: + allowed_amount = 0 + + frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount)) + + if loan_details.status == "Disbursed" and not loan_details.is_term_loan: + process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), + loan=self.against_loan) + + if disbursed_amount > loan_details.loan_amount: + topup_amount = disbursed_amount - loan_details.loan_amount + + if topup_amount < 0: + topup_amount = 0 + + if topup_amount > self.disbursed_amount: + topup_amount = self.disbursed_amount + + total_payment = total_payment + topup_amount + + if flt(disbursed_amount) >= loan_details.loan_amount: status = "Disbursed" else: status = "Partially Disbursed" @@ -75,7 +113,8 @@ class LoanDisbursement(AccountsController): frappe.db.set_value("Loan", self.against_loan, { "disbursement_date": self.disbursement_date, "disbursed_amount": disbursed_amount, - "status": status + "status": status, + "total_payment": total_payment }) def make_gl_entries(self, cancel=0, adv_adj=0): @@ -116,3 +155,24 @@ class LoanDisbursement(AccountsController): if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) + +def get_total_pledged_security_value(loan): + update_time = get_datetime() + + loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters = { + "valid_from": ("<=", update_time), + "valid_upto": (">=", update_time) + }, as_list=1)) + + hair_cut_map = frappe._dict(frappe.get_all('Loan Security', + fields=["name", "haircut"], as_list=1)) + + security_value = 0.0 + pledged_securities = get_pledged_security_qty(loan) + + for security, qty in pledged_securities.items(): + security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 + + return security_value diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index b56fa80c7ae..c5111fdc930 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -85,8 +85,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i if no_of_days <= 0: return - pending_principal_amount = loan.total_payment - loan.total_interest_payable \ - - loan.total_amount_paid + pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) payable_interest = interest_per_day * no_of_days diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 49af0ba6ee8..e17e949b3bc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -718,3 +718,7 @@ erpnext.patches.v13_0.delete_report_requested_items_to_order erpnext.patches.v12_0.update_item_tax_template_company erpnext.patches.v13_0.move_branch_code_to_bank_account erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes +erpnext.patches.v13_0.stock_entry_enhancements +erpnext.patches.v12_0.update_state_code_for_daman_and_diu +erpnext.patches.v12_0.rename_lost_reason_detail +erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment diff --git a/erpnext/patches/v12_0/rename_lost_reason_detail.py b/erpnext/patches/v12_0/rename_lost_reason_detail.py new file mode 100644 index 00000000000..044d0232e09 --- /dev/null +++ b/erpnext/patches/v12_0/rename_lost_reason_detail.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("DocType", "Lost Reason Detail"): + frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail") + frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail") + + frappe.db.sql("""INSERT INTO `tabOpportunity Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Opportunity'""") + + frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""") + + frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) + SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason` + FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""") + + frappe.delete_doc("DocType", "Lost Reason Detail") \ No newline at end of file diff --git a/erpnext/patches/v12_0/stock_entry_enhancements.py b/erpnext/patches/v12_0/stock_entry_enhancements.py index d04b3d38622..847d92894be 100644 --- a/erpnext/patches/v12_0/stock_entry_enhancements.py +++ b/erpnext/patches/v12_0/stock_entry_enhancements.py @@ -19,7 +19,7 @@ def create_stock_entry_types(): for purpose in ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture", "Material Consumption for Manufacture", "Manufacture", - "Repack", "Send to Subcontractor", "Send to Warehouse", "Receive at Warehouse"]: + "Repack", "Send to Subcontractor"]: ste_type = frappe.get_doc({ 'doctype': 'Stock Entry Type', diff --git a/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py new file mode 100644 index 00000000000..7450e9cd8c0 --- /dev/null +++ b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py @@ -0,0 +1,22 @@ +import frappe +from erpnext.regional.india import states + +def execute(): + + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + # Update options in gst_state custom field + gst_state = frappe.get_doc('Custom Field', 'Address-gst_state') + gst_state.options = '\n'.join(states) + gst_state.save() + + # Update gst_state and state code in existing address + frappe.db.sql(""" + UPDATE `tabAddress` + SET + gst_state = 'Dadra and Nagar Haveli and Daman and Diu', + gst_state_number = 26 + WHERE gst_state = 'Daman and Diu' + """) \ No newline at end of file diff --git a/erpnext/patches/v13_0/stock_entry_enhancements.py b/erpnext/patches/v13_0/stock_entry_enhancements.py new file mode 100644 index 00000000000..dcc4f956f77 --- /dev/null +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -0,0 +1,27 @@ +# Copyright(c) 2020, Frappe Technologies Pvt.Ltd.and Contributors +# License: GNU General Public License v3.See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("stock", "doctype", "stock_entry") + if frappe.db.has_column("Stock Entry", "add_to_transit"): + frappe.db.sql(""" + UPDATE `tabStock Entry` SET + stock_entry_type = 'Material Transfer', + purpose = 'Material Transfer', + add_to_transit = 1 WHERE stock_entry_type = 'Send to Warehouse' + """) + + frappe.db.sql("""UPDATE `tabStock Entry` SET + stock_entry_type = 'Material Transfer', + purpose = 'Material Transfer' + WHERE stock_entry_type = 'Receive at Warehouse' + """) + + frappe.reload_doc("stock", "doctype", "warehouse_type") + if not frappe.db.exists('Warehouse Type', 'Transit'): + doc = frappe.new_doc('Warehouse Type') + doc.name = 'Transit' + doc.insert() \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py new file mode 100644 index 00000000000..7c07b987f30 --- /dev/null +++ b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py @@ -0,0 +1,10 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + frappe.reload_doc('hr', 'doctype', 'shift_assignment') + frappe.db.sql("update `tabShift Assignment` set end_date=date, start_date=date where date IS NOT NULL and start_date IS NULL and end_date IS NULL;") diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index adb54f26c6d..cc87caeae1a 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -7,27 +7,30 @@ "field_order": [ "salary_component", "abbr", - "statistical_component", "column_break_3", - "deduct_full_tax_on_selected_payroll_date", + "amount", + "section_break_5", + "additional_salary", + "statistical_component", "depends_on_payment_days", - "is_tax_applicable", "exempted_from_income_tax", + "is_tax_applicable", + "column_break_11", "is_flexible_benefit", "variable_based_on_taxable_salary", + "do_not_include_in_total", + "deduct_full_tax_on_selected_payroll_date", "section_break_2", "condition", + "column_break_18", "amount_based_on_formula", "formula", - "amount", - "do_not_include_in_total", + "section_break_19", "default_amount", "additional_amount", + "column_break_24", "tax_on_flexible_benefit", - "tax_on_additional_salary", - "section_break_11", - "additional_salary", - "condition_and_formula_help" + "tax_on_additional_salary" ], "fields": [ { @@ -110,9 +113,11 @@ "read_only": 1 }, { + "collapsible": 1, "depends_on": "eval:doc.is_flexible_benefit != 1", "fieldname": "section_break_2", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Condtion and formula" }, { "allow_on_submit": 1, @@ -181,23 +186,12 @@ "label": "Tax on additional salary", "read_only": 1 }, - { - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fieldname": "section_break_11", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fieldname": "condition_and_formula_help", - "fieldtype": "HTML", - "label": "Condition and Formula Help", - "options": "

Condition and Formula Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" - }, { "fieldname": "additional_salary", "fieldtype": "Link", "label": "Additional Salary ", - "options": "Additional Salary" + "options": "Additional Salary", + "read_only": 1 }, { "default": "0", @@ -207,11 +201,43 @@ "fieldtype": "Check", "label": "Exempted from Income Tax", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Component properties and references ", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "section_break_19", + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_24", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:21:26.300951", + "modified": "2020-07-01 12:13:41.956495", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 4b623e57b48..7b69dbe8d6d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -123,13 +123,13 @@ frappe.ui.form.on("Salary Slip", { doc: frm.doc, callback: function(r, rt) { frm.refresh(); - if (frm.doc.absent_days){ + if (r.message){ frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)); } } }); } -}) +}); frappe.ui.form.on('Salary Slip Timesheet', { time_sheet: function(frm, dt, dn) { diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 27a974ac837..619c45fa4a1 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -20,15 +20,17 @@ "company", "letter_head", "section_break_10", - "salary_slip_based_on_timesheet", "start_date", "end_date", "salary_structure", + "column_break_18", + "salary_slip_based_on_timesheet", "payroll_frequency", - "column_break_15", + "section_break_20", "total_working_days", "unmarked_days", "leave_without_pay", + "column_break_24", "absent_days", "payment_days", "hourly_wages", @@ -200,10 +202,6 @@ "fieldtype": "Date", "label": "End Date" }, - { - "fieldname": "column_break_15", - "fieldtype": "Column Break" - }, { "fieldname": "salary_structure", "fieldtype": "Link", @@ -490,13 +488,25 @@ "fieldtype": "Float", "hidden": 1, "label": "Unmarked days" + }, + { + "fieldname": "section_break_20", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_24", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-07-22 12:41:03.659422", + "modified": "2020-08-11 17:37:54.274384", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 5de2930c1cd..607c3fd9748 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -94,13 +94,6 @@ frappe.ui.form.on("Timesheet", { } }, - company: function(frm) { - frappe.db.get_value('Company', { 'company_name' : frm.doc.company }, 'standard_working_hours') - .then(({ message }) => { - (frappe.working_hours = message.standard_working_hours || 0); - }); - }, - make_invoice: function(frm) { let dialog = new frappe.ui.Dialog({ title: __("Select Item (optional)"), diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 405a33c72ae..69515390266 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -163,9 +163,11 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(me.frm.doc["items"] || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var cumulated_tax_fraction = 0.0; - + var total_inclusive_tax_amount_per_qty = 0; $.each(me.frm.doc["taxes"] || [], function(i, tax) { - tax.tax_fraction_for_current_item = me.get_current_tax_fraction(tax, item_tax_map); + var current_tax_fraction = me.get_current_tax_fraction(tax, item_tax_map); + tax.tax_fraction_for_current_item = current_tax_fraction[0]; + var inclusive_tax_amount_per_qty = current_tax_fraction[1]; if(i==0) { tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item; @@ -176,10 +178,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } cumulated_tax_fraction += tax.tax_fraction_for_current_item; + total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.qty); }); - if(cumulated_tax_fraction && !me.discount_amount_applied) { - item.net_amount = flt(item.amount / (1 + cumulated_tax_fraction)); + if(!me.discount_amount_applied && item.qty && (total_inclusive_tax_amount_per_qty || cumulated_tax_fraction)) { + var amount = flt(item.amount) - total_inclusive_tax_amount_per_qty; + item.net_amount = flt(amount / (1 + cumulated_tax_fraction)); item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0; me.set_in_company_currency(item, ["net_rate", "net_amount"]); @@ -191,6 +195,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ // Get tax fraction for calculating tax exclusive amount // from tax inclusive amount var current_tax_fraction = 0.0; + var inclusive_tax_amount_per_qty = 0; if(cint(tax.included_in_print_rate)) { var tax_rate = this._get_tax_rate(tax, item_tax_map); @@ -205,13 +210,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else if(tax.charge_type == "On Previous Row Total") { current_tax_fraction = (tax_rate / 100.0) * this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_fraction_for_current_item; + } else if (tax.charge_type == "On Item Quantity") { + inclusive_tax_amount_per_qty = flt(tax_rate); } } - if(tax.add_deduct_tax) { - current_tax_fraction *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; + if(tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") { + current_tax_fraction *= -1; + inclusive_tax_amount_per_qty *= -1; } - return current_tax_fraction; + return [current_tax_fraction, inclusive_tax_amount_per_qty]; }, _get_tax_rate: function(tax, item_tax_map) { @@ -360,8 +368,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else if(tax.charge_type == "On Previous Row Total") { current_tax_amount = (tax_rate / 100.0) * this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item; + } else if (tax.charge_type == "On Item Quantity") { + current_tax_amount = tax_rate * item.qty; } - this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount); return current_tax_amount; @@ -573,7 +582,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ var actual_taxes_dict = {}; $.each(this.frm.doc["taxes"] || [], function(i, tax) { - if (tax.charge_type == "Actual") { + if (in_list(["Actual", "On Item Quantity"], tax.charge_type)) { var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount; tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; actual_taxes_dict[tax.idx] = tax_amount; @@ -586,7 +595,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(actual_taxes_dict, function(key, value) { if (value) total_actual_tax += value; }); - + return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total")); } }, diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py index 0ed98b74eef..d6221a80aa2 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -10,8 +10,7 @@ states = [ 'Bihar', 'Chandigarh', 'Chhattisgarh', - 'Dadra and Nagar Haveli', - 'Daman and Diu', + 'Dadra and Nagar Haveli and Daman and Diu', 'Delhi', 'Goa', 'Gujarat', @@ -50,8 +49,7 @@ state_numbers = { "Bihar": "10", "Chandigarh": "04", "Chhattisgarh": "22", - "Dadra and Nagar Haveli": "26", - "Daman and Diu": "25", + "Dadra and Nagar Haveli and Daman and Diu": "26", "Delhi": "07", "Goa": "30", "Gujarat": "24", diff --git a/erpnext/regional/india/gst_state_code_data.json b/erpnext/regional/india/gst_state_code_data.json index 6dab81d6688..ff88e0f9d6c 100644 --- a/erpnext/regional/india/gst_state_code_data.json +++ b/erpnext/regional/india/gst_state_code_data.json @@ -134,15 +134,10 @@ "state_code": "DL", "state_name": "Delhi" }, - { - "state_number": "25", - "state_code": "DD", - "state_name": "Daman and Diu" - }, { "state_number": "26", "state_code": "DN", - "state_name": "Dadra and Nagar Haveli" + "state_name": "Dadra and Nagar Haveli and Daman and Diu" }, { "state_number": "22", diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index fe7e0c807c5..844e34b9caf 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import frappe, re, json from frappe import _ +import erpnext from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words from erpnext.regional.india import states, state_numbers from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount @@ -673,25 +674,34 @@ def update_grand_total_for_rcm(doc, method): if country != 'India': return + if not doc.total_taxes_and_charges: + return + if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ + gst_accounts.get('igst_account') + base_gst_tax = 0 gst_tax = 0 + for tax in doc.get('taxes'): if tax.category not in ("Total", "Valuation and Total"): continue if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: - gst_tax += tax.base_tax_amount_after_discount_amount + base_gst_tax += tax.base_tax_amount_after_discount_amount + gst_tax += tax.tax_amount_after_discount_amount doc.taxes_and_charges_added -= gst_tax doc.total_taxes_and_charges -= gst_tax + doc.base_taxes_and_charges_added -= base_gst_tax + doc.base_total_taxes_and_charges -= base_gst_tax - update_totals(gst_tax, doc) + update_totals(gst_tax, base_gst_tax, doc) -def update_totals(gst_tax, doc): +def update_totals(gst_tax, base_gst_tax, doc): + doc.base_grand_total -= base_gst_tax doc.grand_total -= gst_tax if doc.meta.get_field("rounded_total"): @@ -707,13 +717,17 @@ def update_totals(gst_tax, doc): doc.outstanding_amount = doc.rounded_total or doc.grand_total doc.in_words = money_in_words(doc.grand_total, doc.currency) + doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company)) doc.set_payment_schedule() def make_regional_gl_entries(gl_entries, doc): country = frappe.get_cached_value('Company', doc.company, 'country') if country != 'India': - return + return gl_entries + + if not doc.total_taxes_and_charges: + return gl_entries if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 222dfa1eb78..a3ed4cebb12 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -7,6 +7,8 @@ from frappe import _ from frappe.utils import flt from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html +from six import iteritems +import json def execute(filters=None): return _execute(filters) @@ -21,21 +23,24 @@ def _execute(filters=None): itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) data = [] + added_item = [] for d in item_list: - row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty] - total_tax = 0 - for tax in tax_columns: - item_tax = itemised_tax.get(d.name, {}).get(tax, {}) - total_tax += flt(item_tax.get("tax_amount")) + if (d.parent, d.item_code) not in added_item: + row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty] + total_tax = 0 + for tax in tax_columns: + item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) + total_tax += flt(item_tax.get("tax_amount", 0)) - row += [d.base_net_amount + total_tax] - row += [d.base_net_amount] + row += [d.base_net_amount + total_tax] + row += [d.base_net_amount] - for tax in tax_columns: - item_tax = itemised_tax.get(d.name, {}).get(tax, {}) - row += [item_tax.get("tax_amount", 0)] + for tax in tax_columns: + item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) + row += [item_tax.get("tax_amount", 0)] - data.append(row) + data.append(row) + added_item.append((d.parent, d.item_code)) if data: data = get_merged_data(columns, data) # merge same hsn code data return columns, data @@ -103,7 +108,7 @@ def get_items(filters): match_conditions = " and {0} ".format(match_conditions) - return frappe.db.sql(""" + items = frappe.db.sql(""" select `tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate, `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty, @@ -118,10 +123,9 @@ def get_items(filters): """ % (conditions, match_conditions), filters, as_dict=1) + return items -def get_tax_accounts(item_list, columns, company_currency, - doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): - import json +def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): item_row_map = {} tax_columns = [] invoice_item_row = {} @@ -171,7 +175,7 @@ def get_tax_accounts(item_list, columns, company_currency, for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ + itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({ "tax_amount": flt(item_tax_amount, tax_amount_precision) }) except ValueError: @@ -179,42 +183,32 @@ def get_tax_accounts(item_list, columns, company_currency, tax_columns.sort() for desc in tax_columns: - columns.append(desc + " Amount:Currency/currency:160") + columns.append({ + "label": desc, + "fieldname": frappe.scrub(desc), + "fieldtype": "Float", + "width": 110 + }) - # columns += ["Total Amount:Currency/currency:110"] return itemised_tax, tax_columns def get_merged_data(columns, data): merged_hsn_dict = {} # to group same hsn under one key and perform row addition - add_column_index = [] # store index of columns that needs to be added - tax_col = len(get_columns()) - fields_to_merge = ["stock_qty", "total_amount", "taxable_amount"] # columns for which index needs to be found - - for i,d in enumerate(columns): - # check if fieldname in to_merge list and ignore tax-columns - if i < tax_col and d["fieldname"] in fields_to_merge: - add_column_index.append(i) + result = [] for row in data: - if row[0] in merged_hsn_dict: - to_add_row = merged_hsn_dict.get(row[0]) + merged_hsn_dict.setdefault(row[0], {}) + for i, d in enumerate(columns): + if d['fieldtype'] not in ('Int', 'Float', 'Currency'): + merged_hsn_dict[row[0]][d['fieldname']] = row[i] + else: + if merged_hsn_dict.get(row[0], {}).get(d['fieldname'], ''): + merged_hsn_dict[row[0]][d['fieldname']] += row[i] + else: + merged_hsn_dict[row[0]][d['fieldname']] = row[i] - # add columns from the add_column_index table - for k in add_column_index: - to_add_row[k] += row[k] + for key, value in iteritems(merged_hsn_dict): + result.append(value) - # add tax columns - for k in range(len(columns)): - if tax_col <= k < len(columns): - to_add_row[k] += row[k] - - # update hsn dict with the newly added data - merged_hsn_dict[row[0]] = to_add_row - else: - merged_hsn_dict[row[0]] = row - - # extract data rows to be displayed in report - data = [merged_hsn_dict[d] for d in merged_hsn_dict] - - return data + return result diff --git a/erpnext/selling/desk_page/selling/selling.json b/erpnext/selling/desk_page/selling/selling.json index 225238233a3..4c09ee94e04 100644 --- a/erpnext/selling/desk_page/selling/selling.json +++ b/erpnext/selling/desk_page/selling/selling.json @@ -18,7 +18,7 @@ { "hidden": 0, "label": "Key Reports", - "links": "[\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -44,7 +44,7 @@ "idx": 0, "is_standard": 1, "label": "Selling", - "modified": "2020-06-29 19:26:35.139097", + "modified": "2020-08-15 10:12:53.131621", "modified_by": "Administrator", "module": "Selling", "name": "Selling", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index ca62488a8c4..93d48321739 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -184,6 +184,14 @@ class Customer(TransactionBase): def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return + + past_credit_limits = [d.credit_limit + for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] + + current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] + + if past_credit_limits == current_credit_limits: + return company_record = [] for limit in self.credit_limits: diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 6d34c2ac7f1..5b85187ccb9 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -923,7 +923,7 @@ "fieldname": "lost_reasons", "fieldtype": "Table MultiSelect", "label": "Lost Reasons", - "options": "Lost Reason Detail", + "options": "Quotation Lost Reason Detail", "read_only": 1 } ], @@ -932,7 +932,7 @@ "is_submittable": 1, "links": [], "max_attachments": 1, - "modified": "2020-07-18 04:59:09.960118", + "modified": "2020-07-26 17:46:19.951223", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 449a968a4f9..ab095ebfe08 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -68,7 +68,7 @@ class Quotation(SellingController): def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): if not self.has_sales_order(): - get_lost_reasons = frappe.get_list('Opportunity Lost Reason', + get_lost_reasons = frappe.get_list('Quotation Lost Reason', fields = ["name"]) lost_reasons_lst = [reason.get('name') for reason in get_lost_reasons] frappe.db.set(self, 'status', 'Lost') diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 24326b22560..30e0918ba68 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -86,7 +86,7 @@ erpnext.PointOfSale.PastOrderSummary = class { this.$summary_container.append( `
` ) - + this.$summary_btns = this.$summary_container.find('.summary-btns'); } @@ -110,7 +110,10 @@ erpnext.PointOfSale.PastOrderSummary = class { {fieldname:'print', fieldtype:'Data', label:'Print Preview'} ], primary_action: () => { - this.events.get_frm().print_preview.printit(true); + const frm = this.events.get_frm(); + frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; + frm.print_preview.printit(true); }, primary_action_label: __('Print'), }); @@ -174,7 +177,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
Tax Charges
- ${ + ${ doc.taxes.map((t, i) => { let margin_left = ''; if (i !== 0) margin_left = 'ml-2'; @@ -271,6 +274,7 @@ erpnext.PointOfSale.PastOrderSummary = class { // this.print_dialog.show(); const frm = this.events.get_frm(); frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; frm.print_preview.printit(true); }); } @@ -284,9 +288,9 @@ erpnext.PointOfSale.PastOrderSummary = class { this.$summary_container.find('.print-btn').click(); }); } - + toggle_component(show) { - show ? + show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); } @@ -372,9 +376,9 @@ erpnext.PointOfSale.PastOrderSummary = class { } get_condition_btn_map(after_submission) { - if (after_submission) + if (after_submission) return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }]; - + return [ { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] }, { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']}, @@ -384,7 +388,7 @@ erpnext.PointOfSale.PastOrderSummary = class { load_summary_of(doc, after_submission=false) { this.$summary_wrapper.removeClass("d-none"); - + after_submission ? this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary(); diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 333a563aa5d..002cfe41e18 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -494,13 +494,18 @@ frappe.ui.form.on(cur_frm.doctype, { var dialog = new frappe.ui.Dialog({ title: __("Set as Lost"), fields: [ - {"fieldtype": "Table MultiSelect", - "label": __("Lost Reasons"), - "fieldname": "lost_reason", - "options": "Lost Reason Detail", - "reqd": 1}, - - {"fieldtype": "Text", "label": __("Detailed Reason"), "fieldname": "detailed_reason"}, + { + "fieldtype": "Table MultiSelect", + "label": __("Lost Reasons"), + "fieldname": "lost_reason", + "options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail', + "reqd": 1 + }, + { + "fieldtype": "Text", + "label": __("Detailed Reason"), + "fieldname": "detailed_reason" + }, ], primary_action: function() { var values = dialog.get_values(); diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 7ae5385a23c..f882db60c5d 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -34,6 +34,16 @@ frappe.ui.form.on("Company", { frm.set_query("default_buying_terms", function() { return { filters: { buying: 1 } }; }); + + frm.set_query("default_in_transit_warehouse", function() { + return { + filters:{ + 'warehouse_type' : 'Transit', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); }, company_name: function(frm) { diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 03703fd82ef..4a26a71970c 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -25,6 +25,7 @@ "default_selling_terms", "default_buying_terms", "default_warehouse_for_sales_return", + "default_in_transit_warehouse", "column_break_10", "country", "create_chart_of_accounts_based_on", @@ -733,6 +734,12 @@ "fieldname": "enable_perpetual_inventory_for_non_stock_items", "fieldtype": "Check", "label": "Enable Perpetual Inventory For Non Stock Items" + }, + { + "fieldname": "default_in_transit_warehouse", + "fieldtype": "Link", + "label": "Default In Transit Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-building", @@ -740,7 +747,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2020-06-24 12:45:31.462195", + "modified": "2020-08-06 00:38:08.311216", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 47b41a97ad3..8e707fe3f49 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -140,7 +140,8 @@ class Company(NestedSet): {"warehouse_name": _("All Warehouses"), "is_group": 1}, {"warehouse_name": _("Stores"), "is_group": 0}, {"warehouse_name": _("Work In Progress"), "is_group": 0}, - {"warehouse_name": _("Finished Goods"), "is_group": 0}]: + {"warehouse_name": _("Finished Goods"), "is_group": 0}, + {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}]: if not frappe.db.exists("Warehouse", "{0} - {1}".format(wh_detail["warehouse_name"], self.abbr)): warehouse = frappe.get_doc({ @@ -149,7 +150,8 @@ class Company(NestedSet): "is_group": wh_detail["is_group"], "company": self.name, "parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr) \ - if not wh_detail["is_group"] else "" + if not wh_detail["is_group"] else "", + "warehouse_type" : wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None }) warehouse.flags.ignore_permissions = True warehouse.flags.ignore_mandatory = True diff --git a/erpnext/setup/doctype/quotation_lost_reason_detail/__init__.py b/erpnext/setup/doctype/quotation_lost_reason_detail/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.json b/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.json new file mode 100644 index 00000000000..543214101a9 --- /dev/null +++ b/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2020-07-14 09:21:44.057724", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "lost_reason" + ], + "fields": [ + { + "fieldname": "lost_reason", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Quotation Lost Reason", + "options": "Quotation Lost Reason" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-26 17:58:56.373775", + "modified_by": "Administrator", + "module": "Setup", + "name": "Quotation Lost Reason Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.py b/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.py new file mode 100644 index 00000000000..7bb8d02670e --- /dev/null +++ b/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 QuotationLostReasonDetail(Document): + pass diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index ad063cfc9d1..72ed00293ed 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -95,8 +95,6 @@ def install(country=None): {'doctype': 'Stock Entry Type', 'name': 'Send to Subcontractor', 'purpose': 'Send to Subcontractor'}, {'doctype': 'Stock Entry Type', 'name': 'Material Transfer for Manufacture', 'purpose': 'Material Transfer for Manufacture'}, {'doctype': 'Stock Entry Type', 'name': 'Material Consumption for Manufacture', 'purpose': 'Material Consumption for Manufacture'}, - {'doctype': 'Stock Entry Type', 'name': 'Send to Warehouse', 'purpose': 'Send to Warehouse'}, - {'doctype': 'Stock Entry Type', 'name': 'Receive at Warehouse', 'purpose': 'Receive at Warehouse'}, # Designation {'doctype': 'Designation', 'designation_name': _('CEO')}, @@ -244,7 +242,10 @@ def install(country=None): {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")} + {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, + + # Warehouse Type + {'doctype': 'Warehouse Type', 'name': 'Transit'}, ] from erpnext.setup.setup_wizard.data.industry_type import get_industry_types diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 45526a3910e..d07b3dc4fef 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -123,6 +123,7 @@ "weightage", "slideshow", "website_image", + "website_image_alt", "thumbnail", "cb72", "website_warehouse", @@ -1054,15 +1055,21 @@ "fieldtype": "Data", "label": "Default Manufacturer Part No", "read_only": 1 + }, + { + "fieldname": "website_image_alt", + "fieldtype": "Data", + "label": "Image Description" } ], "has_web_view": 1, "icon": "fa fa-tag", "idx": 2, "image_field": "image", + "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2020-07-31 21:21:10.956453", + "modified": "2020-08-07 14:24:58.384992", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1124,4 +1131,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d7b43bf399b..d209f483536 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -343,7 +343,7 @@ class Item(WebsiteGenerator): if variant: context.variant = frappe.get_doc("Item", variant) - for fieldname in ("website_image", "web_long_description", "description", + for fieldname in ("website_image", "website_image_alt", "web_long_description", "description", "website_specifications"): if context.variant.get(fieldname): value = context.variant.get(fieldname) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index d1f29e364a6..44503d22a3c 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -11,6 +11,7 @@ "naming_series", "title", "material_request_type", + "transfer_status", "customer", "column_break_2", "schedule_date", @@ -303,13 +304,22 @@ "fieldtype": "Link", "label": "Set From Warehouse", "options": "Warehouse" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.add_to_transit == 1", + "fieldname": "transfer_status", + "fieldtype": "Select", + "label": "Transfer Status", + "options": "\nNot Started\nIn Transit\nCompleted", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2020-05-01 20:21:09.990867", + "modified": "2020-08-10 13:27:54.891058", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index 614ecb8a8f9..0d7095875c6 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -1,8 +1,16 @@ frappe.listview_settings['Material Request'] = { - add_fields: ["material_request_type", "status", "per_ordered", "per_received"], + add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"], get_indicator: function(doc) { if(doc.status=="Stopped") { return [__("Stopped"), "red", "status,=,Stopped"]; + } else if(doc.transfer_status && doc.docstatus != 2) { + if (doc.transfer_status == "Not Started") { + return [__("Not Started"), "orange"]; + } else if (doc.transfer_status == "In Transit") { + return [__("In Transit"), "yellow"]; + } else if (doc.transfer_status == "Completed") { + return [__("Completed"), "green"]; + } } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 0) { return [__("Pending"), "orange", "per_ordered,=,0"]; } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) < 100) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 53b986cb72c..9845bc2f705 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -19,7 +19,6 @@ frappe.ui.form.on('Stock Entry', { filters: [ ['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'per_transferred', '<','100'], - ['Stock Entry', 'purpose', '=', 'Send to Warehouse'] ] } }); @@ -171,9 +170,9 @@ frappe.ui.form.on('Stock Entry', { } } - if (frm.doc.docstatus === 1 && frm.doc.purpose == 'Send to Warehouse') { - if (frm.doc.per_transferred < 100) { - frm.add_custom_button(__('Receive at Warehouse Entry'), function() { + if (frm.doc.docstatus === 1) { + if (frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer' && frm.doc.per_transferred < 100) { + frm.add_custom_button('End Transit', function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.stock_entry.stock_entry.make_stock_in_entry", frm: frm @@ -266,6 +265,7 @@ frappe.ui.form.on('Stock Entry', { stock_entry_type: function(frm){ frm.remove_custom_button('Bill of Materials', "Get items from"); frm.events.show_bom_custom_button(frm); + frm.trigger('add_to_transit'); }, purpose: function(frm) { @@ -532,6 +532,26 @@ frappe.ui.form.on('Stock Entry', { target_warehouse_address: function(frm) { erpnext.utils.get_address_display(frm, 'target_warehouse_address', 'target_address_display', false); + }, + + add_to_transit: function(frm) { + if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') { + frm.set_value('stock_entry_type', 'Material Transfer'); + frm.fields_dict.to_warehouse.get_query = function() { + return { + filters:{ + 'warehouse_type' : 'Transit', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }; + frappe.db.get_value('Company', frm.doc.company, 'default_in_transit_warehouse', (r) => { + if (r.default_in_transit_warehouse) { + frm.set_value('to_warehouse', r.default_in_transit_warehouse); + } + }); + } } }) @@ -754,6 +774,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ } erpnext.hide_company(); erpnext.utils.add_item(this.frm); + this.frm.trigger('add_to_transit'); }, scan_barcode: function() { @@ -919,8 +940,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ doc.purpose!='Material Issue'); this.frm.fields_dict["items"].grid.set_column_disp("additional_cost", doc.purpose!='Material Issue'); - this.frm.toggle_reqd("outgoing_stock_entry", - doc.purpose == 'Receive at Warehouse' ? 1: 0); }, supplier: function(doc) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 704ae41bc5f..61e0df67238 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -13,6 +13,7 @@ "stock_entry_type", "outgoing_stock_entry", "purpose", + "add_to_transit", "work_order", "purchase_order", "delivery_note_no", @@ -116,11 +117,12 @@ "reqd": 1 }, { - "depends_on": "eval:doc.purpose == 'Receive at Warehouse'", + "depends_on": "eval:doc.purpose == 'Material Transfer'", "fieldname": "outgoing_stock_entry", "fieldtype": "Link", "label": "Stock Entry (Outward GIT)", - "options": "Stock Entry" + "options": "Stock Entry", + "read_only": 1 }, { "bold": 1, @@ -132,7 +134,7 @@ "label": "Purpose", "oldfieldname": "purpose", "oldfieldtype": "Select", - "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nSend to Warehouse\nReceive at Warehouse", + "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", "read_only": 1 }, { @@ -630,13 +632,21 @@ { "fieldname": "print_settings_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval: doc.purpose=='Material Transfer' && !doc.outgoing_stock_entry", + "fieldname": "add_to_transit", + "fieldtype": "Check", + "label": "Add to Transit", + "no_copy": 1 } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-23 12:56:52.881752", + "modified": "2020-08-11 19:10:07.954981", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 229cf027bd5..30bcccdda65 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -96,6 +96,11 @@ class StockEntry(StockController): self.update_quality_inspection() if self.work_order and self.purpose == "Manufacture": self.update_so_in_serial_number() + + if self.purpose == 'Material Transfer' and self.add_to_transit: + self.set_material_request_transfer_status('In Transit') + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + self.set_material_request_transfer_status('Completed') def on_cancel(self): @@ -116,6 +121,11 @@ class StockEntry(StockController): self.update_quality_inspection() self.delete_auto_created_batches() + if self.purpose == 'Material Transfer' and self.add_to_transit: + self.set_material_request_transfer_status('Not Started') + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + self.set_material_request_transfer_status('In Transit') + def set_job_card_data(self): if self.job_card and not self.work_order: data = frappe.db.get_value('Job Card', @@ -133,7 +143,7 @@ class StockEntry(StockController): def validate_purpose(self): valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor", - "Material Consumption for Manufacture", "Send to Warehouse", "Receive at Warehouse"] + "Material Consumption for Manufacture"] if self.purpose not in valid_purposes: frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) @@ -199,7 +209,8 @@ class StockEntry(StockController): item.set(f, item_details.get(f)) if not item.transfer_qty and item.qty: - item.transfer_qty = item.qty * item.conversion_factor + item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor), + self.precision("transfer_qty", item)) if (self.purpose in ("Material Transfer", "Material Transfer for Manufacture") and not item.serial_no @@ -258,10 +269,10 @@ class StockEntry(StockController): """perform various (sometimes conditional) validations on warehouse""" source_mandatory = ["Material Issue", "Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture", - "Material Consumption for Manufacture", "Send to Warehouse", "Receive at Warehouse"] + "Material Consumption for Manufacture"] target_mandatory = ["Material Receipt", "Material Transfer", "Send to Subcontractor", - "Material Transfer for Manufacture", "Send to Warehouse", "Receive at Warehouse"] + "Material Transfer for Manufacture"] validate_for_manufacture = any([d.bom_no for d in self.get("items")]) @@ -809,7 +820,7 @@ class StockEntry(StockController): def set_items_for_stock_in(self): self.items = [] - if self.outgoing_stock_entry and self.purpose == 'Receive at Warehouse': + if self.outgoing_stock_entry and self.purpose == 'Material Transfer': doc = frappe.get_doc('Stock Entry', self.outgoing_stock_entry) if doc.per_transferred == 100: @@ -1210,13 +1221,25 @@ class StockEntry(StockController): def validate_with_material_request(self): for item in self.get("items"): - if item.material_request: + material_request = item.material_request or None + material_request_item = item.material_request_item or None + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + parent_se = frappe.get_value("Stock Entry Detail", item.ste_detail, ['material_request','material_request_item'],as_dict=True) + if parent_se: + material_request = parent_se.material_request + material_request_item = parent_se.material_request_item + + if material_request: mreq_item = frappe.db.get_value("Material Request Item", - {"name": item.material_request_item, "parent": item.material_request}, + {"name": material_request_item, "parent": material_request}, ["item_code", "warehouse", "idx"], as_dict=True) - if mreq_item.item_code != item.item_code or \ - mreq_item.warehouse != (item.s_warehouse if self.purpose== "Material Issue" else item.t_warehouse): - frappe.throw(_("Item or Warehouse for row {0} does not match Material Request").format(item.idx), + if mreq_item.item_code != item.item_code: + frappe.throw(_("Item for row {0} does not match Material Request").format(item.idx), + frappe.MappingMismatchError) + elif self.purpose == "Material Transfer" and self.add_to_transit: + continue + elif mreq_item.warehouse != (item.s_warehouse if self.purpose == "Material Issue" else item.t_warehouse): + frappe.throw(_("Warehouse for row {0} does not match Material Request").format(item.idx), frappe.MappingMismatchError) def validate_batch(self): @@ -1284,7 +1307,7 @@ class StockEntry(StockController): to fullfill Sales Order {2}.").format(item.item_code, sr, sales_order)) def update_transferred_qty(self): - if self.purpose == 'Receive at Warehouse': + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: stock_entries = {} stock_entries_child_list = [] for d in self.items: @@ -1342,6 +1365,20 @@ class StockEntry(StockController): 'reference_type': reference_type, 'reference_name': reference_name }) + def set_material_request_transfer_status(self, status): + material_requests = [] + if self.outgoing_stock_entry: + parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, 'add_to_transit') + + for item in self.items: + material_request = item.material_request or None + if self.purpose == "Material Transfer" and material_request not in material_requests: + if self.outgoing_stock_entry and parent_se: + material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, 'material_request') + + if material_request and material_request not in material_requests: + material_requests.append(material_request) + frappe.db.set_value('Material Request', material_request, 'transfer_status', status) @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): @@ -1381,12 +1418,19 @@ def move_sample_to_retention_warehouse(company, items): @frappe.whitelist() def make_stock_in_entry(source_name, target_doc=None): + def set_missing_values(source, target): - target.purpose = 'Receive at Warehouse' target.set_stock_entry_type() def update_item(source_doc, target_doc, source_parent): target_doc.t_warehouse = '' + + if source_doc.material_request_item and source_doc.material_request : + add_to_transit = frappe.db.get_value('Stock Entry', source_name, 'add_to_transit') + if add_to_transit: + warehouse = frappe.get_value('Material Request Item', source_doc.material_request_item, 'warehouse') + target_doc.t_warehouse = warehouse + target_doc.s_warehouse = source_doc.t_warehouse target_doc.qty = source_doc.qty - source_doc.transferred_qty diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 8e25804511e..d98870de3eb 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -737,34 +737,6 @@ class TestStockEntry(unittest.TestCase): self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].amount, 0) - def test_goods_in_transit(self): - from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - warehouse = "_Test Warehouse FG 1 - _TC" - - if not frappe.db.exists('Warehouse', warehouse): - create_warehouse("_Test Warehouse FG 1") - - outward_entry = make_stock_entry(item_code="_Test Item", - purpose="Send to Warehouse", - source="_Test Warehouse - _TC", - target="_Test Warehouse 1 - _TC", qty=50, basic_rate=100) - - inward_entry1 = make_stock_in_entry(outward_entry.name) - inward_entry1.items[0].t_warehouse = warehouse - inward_entry1.items[0].qty = 25 - inward_entry1.submit() - - doc = frappe.get_doc('Stock Entry', outward_entry.name) - self.assertEqual(doc.per_transferred, 50) - - inward_entry2 = make_stock_in_entry(outward_entry.name) - inward_entry2.items[0].t_warehouse = warehouse - inward_entry2.items[0].qty = 25 - inward_entry2.submit() - - doc = frappe.get_doc('Stock Entry', outward_entry.name) - self.assertEqual(doc.per_transferred, 100) - def test_gle_for_opening_stock_entry(self): mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True) diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json index edee3c7dc9a..0f2b55ec342 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json @@ -1,156 +1,83 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "Prompt", - "beta": 0, "creation": "2019-03-13 16:23:46.636769", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "purpose" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Material Issue", - "fetch_if_empty": 0, "fieldname": "purpose", "fieldtype": "Select", - "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": "Purpose", - "length": 0, - "no_copy": 0, - "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nSend to Warehouse\nReceive at Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, + "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 + "set_only_once": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-26 12:02:42.144377", + "links": [], + "modified": "2020-08-10 23:24:37.160817", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Type", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Manufacturing Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Stock Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Stock User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html index 0dd4c3505e1..5d46a45053d 100644 --- a/erpnext/templates/generators/item/item_image.html +++ b/erpnext/templates/generators/item/item_image.html @@ -23,7 +23,7 @@ }) {% else %} -{{ product_image(website_image or image or 'no-image.jpg') }} +{{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }} {% endif %} diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 3c82e90cc0d..ea6b00fc58a 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -7,9 +7,9 @@
{% endmacro %} -{% macro product_image(website_image, css_class="") %} +{% macro product_image(website_image, css_class="", alt="") %}
- + {{ alt }}
{% endmacro %} diff --git a/erpnext/www/support/index.html b/erpnext/www/support/index.html index 93da503dbb0..12b4c2c0819 100644 --- a/erpnext/www/support/index.html +++ b/erpnext/www/support/index.html @@ -9,6 +9,33 @@

{{ greeting_subtitle }}

{% endif %}
+
+ + +
@@ -54,5 +81,21 @@ {% endif %} +{% endblock %} -{% endblock %} \ No newline at end of file +{%- block script -%} + +{%- endblock -%} + +{%- block style -%} + +{%- endblock -%}