From 90eb489392482c1055af49749ea013ac66d1341e Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 14 Oct 2020 15:26:34 +0530 Subject: [PATCH 01/45] fix: added filter show in website for filtering product --- erpnext/portal/product_configurator/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 60aa3b64e82..2ece83e94ad 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -14,13 +14,15 @@ def get_field_filter_data(): for f in fields: doctype = f.get_link_doctype() - # apply enable/disable filter + # apply enable/disable/show_in_website filter meta = frappe.get_meta(doctype) filters = {} if meta.has_field('enabled'): filters['enabled'] = 1 if meta.has_field('disabled'): filters['disabled'] = 0 + if meta.has_field('show_in_website'): + filters['show_in_website'] = 1 values = [d.name for d in frappe.get_all(doctype, filters)] filter_data.append([f, values]) From 4b8d4a1cbe1aa2743dac66968789d93e0b7a1d8b Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 14 Oct 2020 15:50:26 +0530 Subject: [PATCH 02/45] fix: setting user precision instead of 2 (#23630) * fix: setting user precision instead of default * fix: removing unused imports --- erpnext/projects/report/billing_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py index 76379f1de2e..1cfa1ebe5af 100644 --- a/erpnext/projects/report/billing_summary.py +++ b/erpnext/projects/report/billing_summary.py @@ -136,6 +136,7 @@ def get_timesheet_details(filters, timesheet_list): return timesheet_details_map def get_billable_and_total_duration(activity, start_time, end_time): + precision = frappe.get_precision("Timesheet Detail", "hours") activity_duration = time_diff_in_hours(end_time, start_time) billing_duration = 0.0 if activity.billable: @@ -143,4 +144,4 @@ def get_billable_and_total_duration(activity, start_time, end_time): if activity_duration != activity.billing_hours: billing_duration = activity_duration * activity.billing_hours / activity.hours - return flt(activity_duration, 2), flt(billing_duration, 2) \ No newline at end of file + return flt(activity_duration, precision), flt(billing_duration, precision) \ No newline at end of file From f956a2cf72ac3aabae35db517da2bb0402b40650 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Oct 2020 01:19:11 +0530 Subject: [PATCH 03/45] fix: extra material received against send to warehouse entry --- .../stock/doctype/stock_entry/stock_entry.py | 91 ++++++------------- .../doctype/stock_entry/test_stock_entry.py | 27 +++++- 2 files changed, 54 insertions(+), 64 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 961f5f45325..c3be6eb2871 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext import frappe.defaults from frappe import _ -from frappe.utils import cstr, cint, flt, comma_or, getdate, nowdate, formatdate, format_time +from frappe.utils import cstr, cint, flt, comma_or, getdate, nowdate, formatdate, format_time, get_link_to_form from erpnext.stock.utils import get_incoming_rate from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError, get_valuation_rate from erpnext.stock.get_item_details import get_bin_details, get_default_cost_center, get_conversion_factor, get_reserved_qty_for_so @@ -27,6 +27,7 @@ class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass +class ExtraMaterialReceived(frappe.ValidationError): pass from erpnext.controllers.stock_controller import StockController @@ -35,6 +36,11 @@ form_grid_templates = { } class StockEntry(StockController): + def __init__(self, *args, **kwargs): + """To initialize the status updater.""" + super(StockEntry, self).__init__(*args, **kwargs) + self.status_updater = [] + def get_feed(self): return self.stock_entry_type @@ -51,7 +57,6 @@ class StockEntry(StockController): self.validate_purpose() self.validate_item() self.validate_customer_provided_item() - self.validate_qty() self.set_transfer_qty() self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("stock_uom", "transfer_qty") @@ -122,6 +127,22 @@ class StockEntry(StockController): self.from_bom = 1 self.bom_no = data.bom_no + def limits_crossed_error(self, args, item, qty_or_amount): + """To override the method limits_crossed_error which is defined in the status_updater.""" + """Raise the exception for extra material transfer against the send to warehouse.""" + + send_to_ste = frappe.bold(get_link_to_form("Stock Entry", self.outgoing_stock_entry)) + message = _("For more details please check the send to warehouse document {0}.").format(send_to_ste) + + frappe.throw(_('For the item {0}, the received quantity {1} is more than the sent quantity {2}. {3}{4}') + .format( + frappe.bold(item.get('item_code')), + frappe.bold((item[args["target_field"]])), + frappe.bold(item[args["target_ref_field"]]), + '
', + message + ), ExtraMaterialReceived, title = _('Extra Materials Transferred')) + def validate_work_order_status(self): pro_doc = frappe.get_doc("Work Order", self.work_order) if pro_doc.status == 'Completed': @@ -205,33 +226,6 @@ class StockEntry(StockController): frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), frappe.MandatoryError) - def validate_qty(self): - manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"] - - if self.purpose in manufacture_purpose and self.work_order: - if not frappe.get_value('Work Order', self.work_order, 'skip_transfer'): - item_code = [] - for item in self.items: - if cstr(item.t_warehouse) == '': - req_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order, 'item_code': item.item_code}, fields=["item_code"]) - - transferred_materials = frappe.db.sql(""" - select - sum(qty) as qty - from `tabStock Entry` se,`tabStock Entry Detail` sed - where - se.name = sed.parent and se.docstatus=1 and - (se.purpose='Material Transfer for Manufacture' or se.purpose='Manufacture') - and sed.item_code=%s and se.work_order= %s and ifnull(sed.t_warehouse, '') != '' - """, (item.item_code, self.work_order), as_dict=1) - - stock_qty = flt(item.qty) - trans_qty = flt(transferred_materials[0].qty) - if req_items: - if stock_qty > trans_qty: - item_code.append(item.item_code) - def validate_fg_completed_qty(self): if self.purpose == "Manufacture" and self.work_order: production_item = frappe.get_value('Work Order', self.work_order, 'production_item') @@ -1296,37 +1290,7 @@ class StockEntry(StockController): def update_transferred_qty(self): if self.purpose == 'Receive at Warehouse': - stock_entries = {} - stock_entries_child_list = [] - for d in self.items: - if not (d.against_stock_entry and d.ste_detail): - continue - - stock_entries_child_list.append(d.ste_detail) - transferred_qty = frappe.get_all("Stock Entry Detail", fields = ["sum(qty) as qty"], - filters = { 'against_stock_entry': d.against_stock_entry, - 'ste_detail': d.ste_detail,'docstatus': 1}) - - stock_entries[(d.against_stock_entry, d.ste_detail)] = (transferred_qty[0].qty - if transferred_qty and transferred_qty[0] else 0.0) or 0.0 - - if not stock_entries: return None - - cond = '' - for data, transferred_qty in stock_entries.items(): - cond += """ WHEN (parent = %s and name = %s) THEN %s - """ %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty) - - if cond and stock_entries_child_list: - frappe.db.sql(""" UPDATE `tabStock Entry Detail` - SET - transferred_qty = CASE {cond} END - WHERE - name in ({ste_details}) """.format(cond=cond, - ste_details = ','.join(['%s'] * len(stock_entries_child_list))), - tuple(stock_entries_child_list)) - - args = { + self.status_updater.append({ 'source_dt': 'Stock Entry Detail', 'target_field': 'transferred_qty', 'target_ref_field': 'qty', @@ -1335,10 +1299,11 @@ class StockEntry(StockController): 'target_parent_dt': 'Stock Entry', 'target_parent_field': 'per_transferred', 'source_field': 'qty', - 'percent_join_field': 'against_stock_entry' - } + 'percent_join_field': 'against_stock_entry', + 'no_allowance': 1 + }) - self._update_percent_field_in_targets(args, update_modified=True) + self.update_prevdoc_status() def update_quality_inspection(self): if self.inspection_required: diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 84f535912d4..e9778f6c8ca 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -14,7 +14,8 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import from erpnext.stock.doctype.item.test_item import set_item_variant_settings, make_item_variant, create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.accounts.doctype.account.test_account import get_inventory_account -from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry +from erpnext.stock.doctype.stock_entry.stock_entry import (move_sample_to_retention_warehouse, + make_stock_in_entry, ExtraMaterialReceived) from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from six import iteritems @@ -871,6 +872,30 @@ class TestStockEntry(unittest.TestCase): doc = frappe.get_doc('Stock Entry', outward_entry.name) self.assertEqual(doc.per_transferred, 100) + def test_raise_extra_transfer_materials(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() + + inward_entry2 = make_stock_in_entry(outward_entry.name) + inward_entry2.items[0].t_warehouse = warehouse + inward_entry2.items[0].qty = 35 + + self.assertRaises(ExtraMaterialReceived, inward_entry2.submit) + print(inward_entry2.name) + 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) From 8d7e26c7dda90609a798d6c2c572b63f7520b94c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Oct 2020 14:14:27 +0530 Subject: [PATCH 04/45] fix: serial no field is blank in stock reconciliation --- .../doctype/stock_reconciliation/stock_reconciliation.js | 4 ++++ .../doctype/stock_reconciliation/stock_reconciliation.py | 2 ++ .../doctype/stock_reconciliation/test_stock_reconciliation.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 0475ea7a2ec..bf8a59b2722 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -109,6 +109,10 @@ frappe.ui.form.on("Stock Reconciliation", { frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos); + + if (frm.doc.purpose == "Stock Reconciliation") { + frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos); + } } }); } diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 003403a2f8a..f502dbad8bb 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -68,6 +68,8 @@ class StockReconciliation(StockController): if item_dict.get("serial_nos"): item.current_serial_no = item_dict.get("serial_nos") + if self.purpose == "Stock Reconciliation": + item.serial_no = item.current_serial_no item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 8b073ec5ab4..27908016407 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -131,7 +131,7 @@ class TestStockReconciliation(unittest.TestCase): to_delete_records.append(sr.name) sr = create_stock_reconciliation(item_code=serial_item_code, - warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos)) + warehouse = serial_warehouse, qty=5, rate=300) # print(sr.name) serial_nos1 = get_serial_nos(sr.items[0].serial_no) From 433faa705e11d42ffac1bcf76c082cdaccd7c600 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Oct 2020 15:47:21 +0530 Subject: [PATCH 05/45] fix: overproduction, not allowed to transfer extra materials --- .../stock/doctype/stock_entry/stock_entry.py | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f891aea7c3a..8afb3fbe8e5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -838,6 +838,8 @@ class StockEntry(StockController): frappe.throw(_("Posting date and posting time is mandatory")) self.set_work_order_details() + self.flags.backflush_based_on = frappe.db.get_single_value("Manufacturing Settings", + "backflush_raw_materials_based_on") if self.bom_no: @@ -851,14 +853,14 @@ class StockEntry(StockController): item["to_warehouse"] = self.pro_doc.wip_warehouse self.add_to_stock_entry_detail(item_dict) - elif (self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") - and not self.pro_doc.skip_transfer and frappe.db.get_single_value("Manufacturing Settings", - "backflush_raw_materials_based_on")== "Material Transferred for Manufacture"): + elif (self.work_order and (self.purpose == "Manufacture" + or self.purpose == "Material Consumption for Manufacture") and not self.pro_doc.skip_transfer + and self.flags.backflush_based_on == "Material Transferred for Manufacture"): self.get_transfered_raw_materials() - elif self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") and \ - frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")== "BOM" and \ - frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1: + elif (self.work_order and (self.purpose == "Manufacture" or + self.purpose == "Material Consumption for Manufacture") and self.flags.backflush_based_on== "BOM" + and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1): self.get_unconsumed_raw_materials() else: if not self.fg_completed_qty: @@ -1102,7 +1104,6 @@ class StockEntry(StockController): for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse): if (qty > req_qty): - qty = req_qty qty-= d.get(item.warehouse) if qty > 0: @@ -1127,11 +1128,22 @@ class StockEntry(StockController): """ item_dict = self.get_pro_order_required_items() max_qty = flt(self.pro_doc.qty) + + allow_overproduction = False + overproduction_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", + "overproduction_percentage_for_work_order")) + + to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(self.fg_completed_qty) + transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100) + + if transfer_limit_qty >= to_transfer_qty: + allow_overproduction = True + for item, item_details in iteritems(item_dict): pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty) desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty - if desire_to_transfer <= pending_to_issue: + if desire_to_transfer <= pending_to_issue or allow_overproduction: item_dict[item]["qty"] = desire_to_transfer elif pending_to_issue > 0: item_dict[item]["qty"] = pending_to_issue From c7ec475429efabf0e91913721c4e12a5c6bbf317 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Oct 2020 19:00:02 +0530 Subject: [PATCH 06/45] fix: manually set serial nos override with current available serial nos --- .../doctype/stock_reconciliation/stock_reconciliation.js | 7 ++++++- .../doctype/stock_reconciliation/stock_reconciliation.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index bf8a59b2722..0b6610cf85b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -110,7 +110,7 @@ frappe.ui.form.on("Stock Reconciliation", { frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos); - if (frm.doc.purpose == "Stock Reconciliation") { + if (frm.doc.purpose == "Stock Reconciliation" && !d.serial_no) { frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos); } } @@ -187,6 +187,11 @@ frappe.ui.form.on("Stock Reconciliation Item", { frappe.model.set_value(cdt, cdn, "batch_no", ""); } + if (child.serial_no) { + frappe.model.set_value(cdt, cdn, "serial_no", ""); + frappe.model.set_value(cdt, cdn, "current_serial_no", ""); + } + frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index f502dbad8bb..2b4780437f4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -68,7 +68,7 @@ class StockReconciliation(StockController): if item_dict.get("serial_nos"): item.current_serial_no = item_dict.get("serial_nos") - if self.purpose == "Stock Reconciliation": + if self.purpose == "Stock Reconciliation" and not item.serial_no: item.serial_no = item.current_serial_no item.current_qty = item_dict.get("qty") From 28a5169646be4c88a5306a066bc59d27a348f2f4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Oct 2020 21:54:53 +0530 Subject: [PATCH 07/45] fix: remove repetative code --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 08553675f4c..8538b00fd6e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -208,7 +208,6 @@ class PurchaseReceipt(BuyingController): stock_rbnb = self.get_company_default("stock_received_but_not_billed") stock_rbnb_currency = get_account_currency(stock_rbnb) - cogs_account = self.get_company_default("default_expense_account") landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") From 3ae9bbc0a70b235b49a0136d5cd543ef827b940d Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 16 Oct 2020 11:54:36 +0530 Subject: [PATCH 08/45] fix: fuel expense amount of vehicle log (#23634) * fix: fuel expense amount of vehicle log * fix: undefined var employee_id in test * fix: test --- .../doctype/vehicle_log/test_vehicle_log.py | 48 +++++++++++++++---- erpnext/hr/doctype/vehicle_log/vehicle_log.py | 2 +- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py index e9dc7764f7b..cf0048c1a76 100644 --- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py @@ -6,18 +6,28 @@ from __future__ import unicode_literals import frappe import unittest from frappe.utils import nowdate,flt, cstr,random_string +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim class TestVehicleLog(unittest.TestCase): + def setUp(self): + employee_id = frappe.db.sql("""select name from `tabEmployee` where name='testdriver@example.com'""") + self.employee_id = employee_id[0][0] if employee_id else None + + if not self.employee_id: + self.employee_id = make_employee("testdriver@example.com", company="_Test Company") + + self.license_plate = get_vehicle(self.employee_id) + + def tearDown(self): + frappe.delete_doc("Vehicle", self.license_plate, force=1) + frappe.delete_doc("Employee", self.employee_id, force=1) + def test_make_vehicle_log_and_syncing_of_odometer_value(self): - employee_id = frappe.db.sql("""select name from `tabEmployee` where status='Active' order by modified desc limit 1""") - employee_id = employee_id[0][0] if employee_id else None - - license_plate = get_vehicle(employee_id) - vehicle_log = frappe.get_doc({ "doctype": "Vehicle Log", - "license_plate": cstr(license_plate), - "employee":employee_id, + "license_plate": cstr(self.license_plate), + "employee": self.employee_id, "date":frappe.utils.nowdate(), "odometer":5010, "fuel_qty":frappe.utils.flt(50), @@ -27,7 +37,7 @@ class TestVehicleLog(unittest.TestCase): vehicle_log.submit() #checking value of vehicle odometer value on submit. - vehicle = frappe.get_doc("Vehicle", license_plate) + vehicle = frappe.get_doc("Vehicle", self.license_plate) self.assertEqual(vehicle.last_odometer, vehicle_log.odometer) #checking value vehicle odometer on vehicle log cancellation. @@ -40,6 +50,28 @@ class TestVehicleLog(unittest.TestCase): self.assertEqual(vehicle.last_odometer, current_odometer - distance_travelled) + vehicle_log.delete() + + def test_vehicle_log_fuel_expense(self): + vehicle_log = frappe.get_doc({ + "doctype": "Vehicle Log", + "license_plate": cstr(self.license_plate), + "employee": self.employee_id, + "date": frappe.utils.nowdate(), + "odometer":5010, + "fuel_qty":frappe.utils.flt(50), + "price": frappe.utils.flt(500) + }) + vehicle_log.save() + vehicle_log.submit() + + expense_claim = make_expense_claim(vehicle_log.name) + fuel_expense = expense_claim.expenses[0].amount + self.assertEqual(fuel_expense, 50*500) + + vehicle_log.cancel() + frappe.delete_doc("Expense Claim", expense_claim.name) + frappe.delete_doc("Vehicle Log", vehicle_log.name) def get_vehicle(employee_id): license_plate=random_string(10).upper() diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.py b/erpnext/hr/doctype/vehicle_log/vehicle_log.py index 8affab2a18d..04c94e37d5a 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.py @@ -32,7 +32,7 @@ def make_expense_claim(docname): vehicle_log = frappe.get_doc("Vehicle Log", docname) service_expense = sum([flt(d.expense_amount) for d in vehicle_log.service_detail]) - claim_amount = service_expense + flt(vehicle_log.price) + claim_amount = service_expense + (flt(vehicle_log.price) * flt(vehicle_log.fuel_qty) or 1) if not claim_amount: frappe.throw(_("No additional expenses has been added")) From 334c282f7b4ec7d79ed754b5cfca46b0af226574 Mon Sep 17 00:00:00 2001 From: Sun Howwrongbum Date: Mon, 19 Oct 2020 11:30:48 +0530 Subject: [PATCH 09/45] fix: consider rounded_total in returns (#23631) Co-authored-by: Nabin Hait --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index d4d40653e77..7f5603b58b0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -570,7 +570,8 @@ class SalesInvoice(SellingController): def validate_pos(self): if self.is_return: - if flt(self.paid_amount) + flt(self.write_off_amount) - flt(self.grand_total) > \ + invoice_total = self.rounded_total or self.grand_total + if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > \ 1.0/(10.0**(self.precision("grand_total") + 1.0)): frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total")) From 8e7a755aebdc1e8e11f529c6579822623a105380 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 19 Oct 2020 11:37:22 +0530 Subject: [PATCH 10/45] feat: show only available items in point of sale (#23667) * feat: show available items in pos * feat: show selected pos profile on pos screen * fix: codacy * fix: codacy --- .../doctype/pos_profile/pos_profile.json | 11 +- .../pos_profile_user/pos_profile_user.json | 150 ++++-------------- erpnext/accounts/doctype/sales_invoice/pos.py | 11 +- erpnext/accounts/page/pos/pos.js | 11 +- .../page/point_of_sale/point_of_sale.py | 45 ++++-- 5 files changed, 91 insertions(+), 137 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index fba1bed9dd1..2f5bef56e10 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "Prompt", "creation": "2013-05-24 12:15:51", @@ -22,6 +23,7 @@ "allow_user_to_edit_discount", "allow_print_before_pay", "display_items_in_stock", + "hide_unavailable_items", "section_break_15", "applicable_for_users", "section_break_11", @@ -389,11 +391,18 @@ "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category" + }, + { + "default": "0", + "fieldname": "hide_unavailable_items", + "fieldtype": "Check", + "label": "Hide Unavailable Items" } ], "icon": "icon-cog", "idx": 1, - "modified": "2020-01-24 15:52:03.797701", + "links": [], + "modified": "2020-10-16 04:33:57.283873", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.json b/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.json index 2fb66d227b1..f3a21d6475c 100644 --- a/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.json +++ b/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.json @@ -1,123 +1,39 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-10-27 16:46:06.060930", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-10-27 16:46:06.060930", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "default", + "user" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default", - "fieldtype": "Check", - "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": "Default", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "user", - "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": "User", - "length": 0, - "no_copy": 0, - "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User" } - ], - "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": "2017-11-23 17:13:16.005475", - "modified_by": "Administrator", - "module": "Accounts", - "name": "POS Profile User", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 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 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-10-16 04:33:27.594859", + "modified_by": "Administrator", + "module": "Accounts", + "name": "POS Profile User", + "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/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index f371bd425c5..64c7f491bb7 100755 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -175,6 +175,13 @@ def get_items_list(pos_profile, company): if args_list: cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list))) + bin_join = bin_cond = "" + if pos_profile.get('hide_unavailable_items'): + bin_join = ",`tabBin` b" + bin_cond = "and i.item_code = b.item_code and ifnull(b.actual_qty, 0) > 0 " + if pos_profile.get('warehouse'): + bin_cond += "and b.warehouse = {}".format(frappe.db.escape(pos_profile.get('warehouse'))) + return frappe.db.sql(""" select i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no, @@ -186,11 +193,13 @@ def get_items_list(pos_profile, company): left join `tabItem Default` id on id.parent = i.name and id.company = %s left join `tabItem Tax` it on it.parent = i.name left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom + {bin_join} where i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 {cond} + {bin_cond} group by i.item_code - """.format(cond=cond), tuple([company] + args_list), as_dict=1) + """.format(cond=cond, bin_join=bin_join, bin_cond=bin_cond), tuple([company] + args_list), as_dict=1) def get_item_groups(pos_profile): diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 1e82e54cdf8..b9b1d293d47 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -81,7 +81,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ me.page.set_indicator(__("Online"), "green") } } - }) + }); }, onload: function () { @@ -278,6 +278,14 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ }) }, + set_pos_profile_title(pos_profile) { + this.page.set_title_sub( + ` + ${pos_profile} + ` + ); + }, + get_data_from_server: function (callback) { var me = this; frappe.call({ @@ -286,6 +294,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ freeze_message: __("Master data syncing, it might take some time"), callback: function (r) { localStorage.setItem('doc', JSON.stringify(r.message.doc)); + me.set_pos_profile_title(r.message.pos_profile.name); me.init_master_data(r) me.set_interval_for_si_sync(); me.check_internet_connection(); diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 8e130ba4246..8bebc8f1fce 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -16,7 +16,9 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p display_items_in_stock = 0 if pos_profile: - warehouse, display_items_in_stock = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'display_items_in_stock']) + warehouse, display_items_in_stock, hide_unavailable_items = frappe.db.get_value( + 'POS Profile', pos_profile, ['warehouse', 'display_items_in_stock', 'hide_unavailable_items'] + ) if not frappe.db.exists('Item Group', item_group): item_group = get_root_of('Item Group') @@ -37,24 +39,31 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt']) # locate function is used to sort by closest match from the beginning of the value + bin_join = bin_cond = "" + if hide_unavailable_items: + bin_join = ",`tabBin` b" + bin_cond = "and i.item_code = b.item_code and ifnull(b.actual_qty, 0) > 0 " + if warehouse: + bin_cond += "and b.warehouse = {}".format(frappe.db.escape(warehouse)) + result = [] items_data = frappe.db.sql(""" SELECT - name AS item_code, - item_name, - stock_uom, - image AS item_image, - idx AS idx, - is_stock_item + i.name AS item_code, + i.item_name, + i.stock_uom, + i.image AS item_image, + i.idx AS idx, + i.is_stock_item FROM - `tabItem` + `tabItem` i {bin_join} WHERE disabled = 0 - AND has_variants = 0 - AND is_sales_item = 1 - AND item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt}) - AND {condition} + AND i.has_variants = 0 + AND i.is_sales_item = 1 + AND i.item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt}) + {condition} {bin_cond} ORDER BY idx desc LIMIT @@ -64,7 +73,9 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p page_length=page_length, lft=lft, rgt=rgt, - condition=condition + condition=condition, + bin_join=bin_join, + bin_cond=bin_cond ), as_dict=1) if items_data: @@ -154,16 +165,16 @@ def search_serial_or_batch_or_barcode_number(search_value): def get_conditions(item_code, serial_no, batch_no, barcode): if serial_no or batch_no or barcode: - return "name = {0}".format(frappe.db.escape(item_code)) + return "and i.name = {0}".format(frappe.db.escape(item_code)) - return """(name like {item_code} - or item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%')) + return ("""and (i.name like {item_code} or i.item_name like {item_code})""" + .format(item_code=frappe.db.escape('%' + item_code + '%'))) def get_item_group_condition(pos_profile): cond = "and 1=1" item_groups = get_item_groups(pos_profile) if item_groups: - cond = "and item_group in (%s)"%(', '.join(['%s']*len(item_groups))) + cond = "and i.item_group in (%s)"%(', '.join(['%s']*len(item_groups))) return cond % tuple(item_groups) From 203fc2b940b29b371fd40db88143679b2f284ea6 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 20 Oct 2020 10:56:38 +0530 Subject: [PATCH 11/45] fix: removed extra space from label "Rate" (#23685) --- .../supplier_quotation_item/supplier_quotation_item.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index b50e834ec73..794a8181ead 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -237,7 +236,7 @@ "fieldname": "rate", "fieldtype": "Currency", "in_list_view": 1, - "label": "Rate ", + "label": "Rate", "oldfieldname": "import_rate", "oldfieldtype": "Currency", "options": "currency" @@ -531,9 +530,9 @@ } ], "idx": 1, + "index_web_pages_for_search": 1, "istable": 1, - "links": [], - "modified": "2020-04-07 18:35:51.175947", + "modified": "2020-10-19 17:16:06.731729", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", From ab5d5969594684971c43a4e98dada1237bfebd26 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 22 Oct 2020 16:28:41 +0530 Subject: [PATCH 12/45] fix: Add Taxes if missing via Update Items (#23705) * fix: Add Taxes if missing via Update Items * chore: PO Test for adding tax row via Update Items * chore: SO Test for adding tax row via Update Items --- .../purchase_order/test_purchase_order.py | 50 ++++++++-- erpnext/controllers/accounts_controller.py | 27 ++++++ .../doctype/sales_order/test_sales_order.py | 91 ++++++++++++++++++- erpnext/stock/get_item_details.py | 5 + 4 files changed, 166 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index f568a182ed7..cb745d6dba8 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -203,9 +203,39 @@ class TestPurchaseOrder(unittest.TestCase): frappe.set_user("Administrator") def test_update_child_with_tax_template(self): + """ + Test Action: Create a PO with one item having its tax account head already in the PO. + Add the same item + new item with tax template via Update Items. + Expected result: First Item's tax row is updated. New tax row is added for second Item. + """ + if not frappe.db.exists("Item", "Test Item with Tax"): + make_item("Test Item with Tax", { + 'is_stock_item': 1, + }) + + if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}): + frappe.get_doc({ + 'doctype': 'Item Tax Template', + 'title': 'Test Update Items Template', + 'company': '_Test Company', + 'taxes': [ + { + 'tax_type': "_Test Account Service Tax - _TC", + 'tax_rate': 10, + } + ] + }).insert() + + new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") + + new_item_with_tax.append("taxes", { + "item_tax_template": "Test Update Items Template", + "valid_from": nowdate() + }) + new_item_with_tax.save() + tax_template = "_Test Account Excise Duty @ 10" item = "_Test Item Home Desktop 100" - if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): item_doc = frappe.get_doc("Item", item) item_doc.append("taxes", { @@ -237,17 +267,25 @@ class TestPurchaseOrder(unittest.TestCase): items = json.dumps([ {'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name}, - {'item_code' : item, 'rate' : 100, 'qty' : 1} # added item + {'item_code' : item, 'rate' : 100, 'qty' : 1}, # added item whose tax account head already exists in PO + {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO ]) update_child_qty_rate('Purchase Order', items, po.name) po.reload() - self.assertEqual(po.taxes[0].tax_amount, 60) - self.assertEqual(po.taxes[0].total, 660) + self.assertEqual(po.taxes[0].tax_amount, 70) + self.assertEqual(po.taxes[0].total, 770) + self.assertEqual(po.taxes[1].account_head, "_Test Account Service Tax - _TC") + self.assertEqual(po.taxes[1].tax_amount, 70) + self.assertEqual(po.taxes[1].total, 840) + # teardown frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL - where parent = %(item)s and item_tax_template = %(tax)s""", - {"item": item, "tax": tax_template}) + where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}) + po.cancel() + po.delete() + new_item_with_tax.delete() + frappe.get_doc("Item Tax Template", "Test Update Items Template").delete() def test_update_qty(self): po = create_purchase_order() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 12b8f697cde..31045a97671 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1139,6 +1139,31 @@ def set_child_tax_template_and_map(item, child_item, parent_doc): if child_item.get("item_tax_template"): child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True) +def add_taxes_from_tax_template(child_item, parent_doc): + add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template") + + if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template: + tax_map = json.loads(child_item.get("item_tax_rate")) + for tax_type in tax_map: + tax_rate = flt(tax_map[tax_type]) + taxes = parent_doc.get('taxes') or [] + # add new row for tax head only if missing + found = any(tax.account_head == tax_type for tax in taxes) + if not found: + tax_row = parent_doc.append("taxes", {}) + tax_row.update({ + "description" : str(tax_type).split(' - ')[0], + "charge_type" : "On Net Total", + "account_head" : tax_type, + "rate" : tax_rate + }) + if parent_doc.doctype == "Purchase Order": + tax_row.update({ + "category" : "Total", + "add_deduct_tax" : "Add" + }) + tax_row.db_insert() + def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): """ Returns a Sales Order Item child item containing the default values @@ -1153,6 +1178,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = item.stock_uom set_child_tax_template_and_map(item, child_item, p_doc) + add_taxes_from_tax_template(child_item, p_doc) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") @@ -1176,6 +1202,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation set_child_tax_template_and_map(item, child_item, p_doc) + add_taxes_from_tax_template(child_item, p_doc) return child_item def validate_and_delete_children(parent, data): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 55458a51e98..fcde0d502e9 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -402,7 +402,7 @@ class TestSalesOrder(unittest.TestCase): trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) - + def test_update_child_with_precision(self): from frappe.model.meta import get_field_precision from frappe.custom.doctype.property_setter.property_setter import make_property_setter @@ -492,6 +492,95 @@ class TestSalesOrder(unittest.TestCase): so.reload() self.assertEqual(so.packed_items[0].qty, 4) + def test_update_child_with_tax_template(self): + """ + Test Action: Create a SO with one item having its tax account head already in the SO. + Add the same item + new item with tax template via Update Items. + Expected result: First Item's tax row is updated. New tax row is added for second Item. + """ + if not frappe.db.exists("Item", "Test Item with Tax"): + make_item("Test Item with Tax", { + 'is_stock_item': 1, + }) + + if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}): + frappe.get_doc({ + 'doctype': 'Item Tax Template', + 'title': 'Test Update Items Template', + 'company': '_Test Company', + 'taxes': [ + { + 'tax_type': "_Test Account Service Tax - _TC", + 'tax_rate': 10, + } + ] + }).insert() + + new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") + + new_item_with_tax.append("taxes", { + "item_tax_template": "Test Update Items Template", + "valid_from": nowdate() + }) + new_item_with_tax.save() + + tax_template = "_Test Account Excise Duty @ 10" + item = "_Test Item Home Desktop 100" + if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): + item_doc = frappe.get_doc("Item", item) + item_doc.append("taxes", { + "item_tax_template": tax_template, + "valid_from": nowdate() + }) + item_doc.save() + else: + # update valid from + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}) + + so = make_sales_order(item_code=item, qty=1, do_not_save=1) + + so.append("taxes", { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 10 + }) + so.insert() + so.submit() + + self.assertEqual(so.taxes[0].tax_amount, 10) + self.assertEqual(so.taxes[0].total, 110) + + old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse") + frappe.db.set_value("Stock Settings", None, "default_warehouse", "_Test Warehouse - _TC") + + items = json.dumps([ + {'item_code' : item, 'rate' : 100, 'qty' : 1, 'docname': so.items[0].name}, + {'item_code' : item, 'rate' : 200, 'qty' : 1}, # added item whose tax account head already exists in PO + {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO + ]) + update_child_qty_rate('Sales Order', items, so.name) + + so.reload() + self.assertEqual(so.taxes[0].tax_amount, 40) + self.assertEqual(so.taxes[0].total, 440) + self.assertEqual(so.taxes[1].account_head, "_Test Account Service Tax - _TC") + self.assertEqual(so.taxes[1].tax_amount, 40) + self.assertEqual(so.taxes[1].total, 480) + + # teardown + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}) + so.cancel() + so.delete() + new_item_with_tax.delete() + frappe.get_doc("Item Tax Template", "Test Update Items Template").delete() + frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value) + def test_warehouse_user(self): frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com") diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 50c8c4e53f4..b1e38b340c5 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -385,6 +385,11 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults={}): else: warehouse = args.get('warehouse') + if not warehouse: + default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") + if frappe.db.get_value("Warehouse", default_warehouse, "company") == args.company: + return default_warehouse + return warehouse def update_barcode_value(out): From eb6fb6fffabc879292862aed0a58aa7d3c1e6c8f Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 22 Oct 2020 16:31:55 +0530 Subject: [PATCH 13/45] fix: cannot auto unlink payments for credit/debit notes (#23690) --- erpnext/controllers/accounts_controller.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 31045a97671..58c7e847910 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -601,8 +601,6 @@ class AccountsController(TransactionBase): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries if self.doctype in ["Sales Invoice", "Purchase Invoice"]: - if self.is_return: return - if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): unlink_ref_doc_from_payment_entries(self) From e9b7b69435b431d774c4c0cf5de7669bf53d20b3 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 23 Oct 2020 11:55:03 +0530 Subject: [PATCH 14/45] fix: Cashier Closing Type Issue --- erpnext/accounts/doctype/cashier_closing/cashier_closing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/cashier_closing/cashier_closing.py b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py index 6de62ee5777..7ad1d3ab831 100644 --- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.py +++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py @@ -23,13 +23,13 @@ class CashierClosing(Document): where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s """, (self.date, self.from_time, self.time, self.user)) self.outstanding_amount = flt(values[0][0] if values else 0) - + def make_calculations(self): total = 0.00 for i in self.payments: total += flt(i.amount) - self.net_amount = total + self.outstanding_amount + self.expense - self.custody + self.returns + self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns) def validate_time(self): if self.from_time >= self.time: From a76b1c530d29f05c37820a7187ea2333f50bb85e Mon Sep 17 00:00:00 2001 From: Aditya Duggal Date: Sat, 24 Oct 2020 11:20:24 +0530 Subject: [PATCH 15/45] fix: None type error if the Pricing Rule applicable_for is None (#23664) * fix: None type error if the Pricing Rule applicable_for is None * fix: sider Co-authored-by: Nabin Hait Co-authored-by: Saqib --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index c5571970595..da0824a97b2 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -49,9 +49,10 @@ class PricingRule(Document): if self.apply_on == apply_on and len(self.get(field) or []) < 1: throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError) - tocheck = frappe.scrub(self.get("applicable_for", "")) - if tocheck and not self.get(tocheck): - throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError) + if self.get("applicable_for", "") is not None: + tocheck = frappe.scrub(self.get("applicable_for", "")) + if tocheck and not self.get(tocheck): + throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError) if self.apply_rule_on_other: o_field = 'other_' + frappe.scrub(self.apply_rule_on_other) From 439c4e11bfa4535710e14416a7f1674f0d451f6d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 24 Oct 2020 22:00:17 +0530 Subject: [PATCH 16/45] fix: Show accounts in financial statements upto level 20 (#23719) --- erpnext/accounts/report/financial_statements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 9e3f3b739c2..c0de29ac1a1 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -294,7 +294,7 @@ def get_accounts(company, root_type): where company=%s and root_type=%s order by lft""", (company, root_type), as_dict=True) -def filter_accounts(accounts, depth=10): +def filter_accounts(accounts, depth=20): parent_children_map = {} accounts_by_name = {} for d in accounts: From 17a8874c509db21a08218cf0e2572e96b3432418 Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 26 Oct 2020 17:07:40 +0530 Subject: [PATCH 17/45] fix: copying po no when mapping doc --- .../doctype/sales_invoice/sales_invoice.py | 1 + erpnext/controllers/selling_controller.py | 40 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7f5603b58b0..2f8b782356d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1395,6 +1395,7 @@ def make_delivery_note(source_name, target_doc=None): def set_missing_values(source, target): target.ignore_pricing_rule = 1 target.run_method("set_missing_values") + target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") def update_item(source_doc, target_doc, source_parent): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 3ebb12541ab..7496081b934 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -365,13 +365,39 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def set_po_nos(self): - if self.doctype in ("Delivery Note", "Sales Invoice") and hasattr(self, "items"): - ref_fieldname = "against_sales_order" if self.doctype == "Delivery Note" else "sales_order" - sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) - if sales_orders: - po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if po_nos and po_nos[0].get('po_no'): - self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no]))) + self.po_no = '' + if self.doctype == 'Sales Invoice' and hasattr(self, "items"): + self.set_pos_for_sales_invoice() + if self.doctype == 'Delivery Note' and hasattr(self, "items"): + self.set_pos_for_delivery_note() + + def set_pos_for_sales_invoice(self): + ref_fieldname1 = "sales_order" + ref_fieldname2 = "delivery_note" + sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) + if sales_orders: + so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) + if so_po_nos and so_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) + delivery_notes = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) + if delivery_notes: + dn_po_nos = frappe.get_all('Delivery Note', 'po_no', filters = {'name': ('in', delivery_notes)}) + if dn_po_nos and dn_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in dn_po_nos if d.po_no]))) + + def set_pos_for_delivery_note(self): + ref_fieldname1 = "against_sales_order" + ref_fieldname2 = "against_sales_invoice" + sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) + sales_invoices = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) + if sales_orders: + so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) + if so_po_nos and so_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) + if sales_invoices: + si_po_nos = frappe.get_all('Sales Invoice', 'po_no', filters = {'name': ('in', sales_invoices)}) + if si_po_nos and si_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in si_po_nos if d.po_no]))) def set_gross_profit(self): if self.doctype == "Sales Order": From 0c511b31fd8204d6feaab79a67dd34abcaec9a7a Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 26 Oct 2020 20:34:48 +0530 Subject: [PATCH 18/45] fix: Remove Production Order reference from Item Validation --- erpnext/stock/doctype/item/item.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index e6634d29fe1..fe47e669261 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -983,9 +983,7 @@ class Item(WebsiteGenerator): if self.stock_ledger_created(): return True - elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}) or \ - frappe.db.get_value("Production Order", - filters={"production_item": self.name, "docstatus": 1}): + elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}): return True def validate_auto_reorder_enabled_in_stock_settings(self): From 1fa8dcb15b0e08133aa650723e761cfc7f5a1db5 Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 28 Oct 2020 11:02:02 +0530 Subject: [PATCH 19/45] fix: override field_map for job card gantt --- .../doctype/job_card/job_card_calendar.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js index cf07698ad6a..f4877fdca0b 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js @@ -8,7 +8,17 @@ frappe.views.calendar["Job Card"] = { "allDay": "allDay", "progress": "progress" }, - gantt: true, + gantt: { + field_map: { + "start": "started_time", + "end": "started_time", + "id": "name", + "title": "subject", + "color": "color", + "allDay": "allDay", + "progress": "progress" + } + }, filters: [ { "fieldtype": "Link", From 6c894f8f413cf3ebbbe525c900c917c198f7dc88 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sat, 30 May 2020 01:02:06 +0530 Subject: [PATCH 20/45] fix(HR) : Filter Leave Type based on allocation for a particular employee (#22050) * table was showing empty with just headers when no leaves allocated, fixed template code * added filters on Leave Type based on leave allocation for a particular employee and to/from dates --- erpnext/hr/doctype/leave_application/leave_application.js | 7 +++++++ .../leave_application/leave_application_dashboard.html | 7 +++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 7b274119244..f88d576fa5c 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -70,6 +70,13 @@ frappe.ui.form.on("Leave Application", { }) ); frm.dashboard.show(); + frm.set_query('leave_type', function(){ + return { + filters : [ + ['leave_type_name', 'in', Object.keys(leave_details)] + ] + } + }); } }, diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html index 295f3b43419..d30e3b9f9c6 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html @@ -1,5 +1,5 @@ -{% if data %} +{% if not jQuery.isEmptyObject(data) %}
{{ __("Allocated Leaves") }}
@@ -11,7 +11,6 @@ - {% for(const [key, value] of Object.entries(data)) { %} @@ -26,6 +25,6 @@ {% } %}
{{ __("Pending Leaves") }} {{ __("Available Leaves") }}
-{% } else { %} +{% else %}

No Leaves have been allocated.

-{% } %} \ No newline at end of file +{% endif %} \ No newline at end of file From 5f148d3d3a08d0777c4d7b2e28f8f08224d1af9a Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Fri, 12 Jun 2020 17:37:59 +0530 Subject: [PATCH 21/45] fix: set_query in leave application (#22197) --- .../hr/doctype/leave_application/leave_application.js | 11 +++++++++-- .../hr/doctype/leave_application/leave_application.py | 8 ++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index f88d576fa5c..4262fd3a10f 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -46,6 +46,7 @@ frappe.ui.form.on("Leave Application", { make_dashboard: function(frm) { var leave_details; + let lwps; if (frm.doc.employee) { frappe.call({ method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details", @@ -61,6 +62,7 @@ frappe.ui.form.on("Leave Application", { if (!r.exc && r.message['leave_approver']) { frm.set_value('leave_approver', r.message['leave_approver']); } + lwps = r.message["lwps"]; } }); $("div").remove(".form-dashboard-section"); @@ -70,12 +72,17 @@ frappe.ui.form.on("Leave Application", { }) ); frm.dashboard.show(); + let allowed_leave_types = Object.keys(leave_details); + + // lwps should be allowed, lwps don't have any allocation + allowed_leave_types = allowed_leave_types.concat(lwps); + frm.set_query('leave_type', function(){ return { filters : [ - ['leave_type_name', 'in', Object.keys(leave_details)] + ['leave_type_name', 'in', allowed_leave_types] ] - } + }; }); } }, diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index cac4f33a237..aa7d7316b48 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -19,7 +19,6 @@ class NotAnOptionalHoliday(frappe.ValidationError): pass from frappe.model.document import Document class LeaveApplication(Document): - def get_feed(self): return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type) @@ -451,9 +450,14 @@ def get_leave_details(employee, date): "pending_leaves": leaves_pending, "remaining_leaves": remaining_leaves} + #is used in set query + lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1}) + lwps = [lwp.name for lwp in lwps] + ret = { 'leave_allocation': leave_allocation, - 'leave_approver': get_leave_approver(employee) + 'leave_approver': get_leave_approver(employee), + 'lwps': lwps } return ret From 82db751a52db5f535687e90121c05037a98675c8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 29 Oct 2020 11:19:34 +0530 Subject: [PATCH 22/45] fix: LMS sign-up link (#23752) --- erpnext/www/lms/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/www/lms/index.html b/erpnext/www/lms/index.html index 7ce3521273f..7a405d80cfe 100644 --- a/erpnext/www/lms/index.html +++ b/erpnext/www/lms/index.html @@ -45,7 +45,7 @@

{{ education_settings.description }}

{% if frappe.session.user == 'Guest' %} - {{_('Sign Up')}} + {{_('Sign Up')}} {% endif %}

From 2f12eed44df30a44512a1e34adec05b7ecd047a4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 29 Oct 2020 15:52:33 +0530 Subject: [PATCH 23/45] fix: subscription test case --- erpnext/accounts/doctype/subscription/test_subscription.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index e11c0c39701..5d73e79035c 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -209,7 +209,7 @@ class TestSubscription(unittest.TestCase): subscription = frappe.new_doc('Subscription') subscription.customer = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start = '2018-01-01' + subscription.start = add_days(nowdate(), -1000) subscription.days_until_due = 1 subscription.insert() subscription.process() # generate first invoice From d8705240a012b940de6b98648c5b72f99409e3e6 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 29 Oct 2020 18:54:06 +0530 Subject: [PATCH 24/45] chore: (Production Plan) Simplify and fix translation in message popup (#23754) --- .../doctype/production_plan/production_plan.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index aa80dcfed24..5a193d7e4bf 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -709,8 +709,12 @@ def get_items_for_material_requests(doc, ignore_existing_ordered_qty=None): mr_items.append(items) if not mr_items: - frappe.msgprint(_("""As raw materials projected quantity is more than required quantity, there is no need to create material request. - Still if you want to make material request, kindly enable Ignore Existing Projected Quantity checkbox""")) + to_enable = frappe.bold(_("Ignore Existing Projected Quantity")) + warehouse = frappe.bold(doc.get('for_warehouse')) + message = _("As there are sufficient raw materials, Material Request is not required for Warehouse {0}.").format(warehouse) + "

" + message += _(" If you still want to proceed, please enable {0}.").format(to_enable) + + frappe.msgprint(message, title=_("Note")) return mr_items From e523cfbb021c5606f6d071de0eec45923378cf95 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 30 Oct 2020 02:47:39 +0530 Subject: [PATCH 25/45] fix: po_detail field has no value for subcontracted stock entry --- .../buying/doctype/purchase_order/test_purchase_order.py | 6 +++++- erpnext/controllers/buying_controller.py | 6 +++--- erpnext/stock/doctype/stock_entry/stock_entry.py | 9 +++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index cb745d6dba8..be5763b1d9c 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -855,7 +855,7 @@ class TestPurchaseOrder(unittest.TestCase): }, { "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item", - "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[1].name + "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos" }, ] @@ -864,6 +864,10 @@ class TestPurchaseOrder(unittest.TestCase): se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) se.submit() + # Test po_detail field has value or not + for item_row in se.items: + self.assertEqual(item_row.po_detail, po.supplied_items[item_row.idx - 1].name) + po_doc = frappe.get_doc("Purchase Order", po.name) for row in po_doc.supplied_items: # Valid that whether transferred quantity is matching with supplied qty or not in the purchase order diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6e05a312352..68fc331e218 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -292,7 +292,7 @@ class BuyingController(StockController): # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) for raw_material in transferred_raw_materials + non_stock_items: - rm_item_key = (raw_material.rm_item_code, item.purchase_order) + rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order) raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) consumed_qty = raw_material_data.get('qty', 0) @@ -881,7 +881,7 @@ def get_backflushed_subcontracted_raw_materials(purchase_orders): purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references) for data in purchase_receipt_supplied_items: - pr_key = (data.rm_item_code, args[0]) + pr_key = (data.rm_item_code, data.main_item_code, args[0]) if pr_key not in backflushed_raw_materials_map: backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({ "qty": 0.0, @@ -907,7 +907,7 @@ def get_backflushed_subcontracted_raw_materials(purchase_orders): def get_supplied_items(item_code, purchase_receipt, references): return frappe.get_all("Purchase Receipt Item Supplied", - fields=["rm_item_code", "consumed_qty", "serial_no", "batch_no"], + fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"], filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)}) def get_asset_item_details(asset_items): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8afb3fbe8e5..3bed73e9258 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -594,6 +594,15 @@ class StockEntry(StockController): if not row.subcontracted_item: frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}") .format(row.idx, frappe.bold(row.item_code))) + elif not row.po_detail: + filters = { + "parent": self.purchase_order, "docstatus": 1, + "rm_item_code": row.item_code, "main_item_code": row.subcontracted_item + } + + po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name") + if po_detail: + row.db_set("po_detail", po_detail) def validate_bom(self): for d in self.get('items'): From c8201eba33f0deea4a2375772237a38cd2a729c2 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Fri, 30 Oct 2020 18:47:52 +0530 Subject: [PATCH 26/45] fix: leave ledger entries (#23782) --- erpnext/patches.txt | 2 +- .../v12_0/generate_leave_ledger_entries.py | 27 ++++++------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index afb6db35f27..1e704c86520 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -635,7 +635,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') erpnext.patches.v12_0.add_default_dashboards erpnext.patches.v12_0.remove_bank_remittance_custom_fields -erpnext.patches.v12_0.generate_leave_ledger_entries +erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020 erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index c5bec19fed4..342c12996d1 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -36,8 +36,7 @@ def generate_allocation_ledger_entries(): for allocation in allocation_list: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}): - allocation.update(dict(doctype="Leave Allocation")) - allocation_obj = frappe.get_doc(allocation) + allocation_obj = frappe.get_doc("Leave Allocation", allocation) allocation_obj.create_leave_ledger_entry() def generate_application_leave_ledger_entries(): @@ -46,8 +45,7 @@ def generate_application_leave_ledger_entries(): for application in leave_applications: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}): - application.update(dict(doctype="Leave Application")) - frappe.get_doc(application).create_leave_ledger_entry() + frappe.get_doc("Leave Application", application.name).create_leave_ledger_entry() def generate_encashment_leave_ledger_entries(): ''' fix ledger entries for missing leave encashment transaction ''' @@ -55,8 +53,7 @@ def generate_encashment_leave_ledger_entries(): for encashment in leave_encashments: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): - encashment.update(dict(doctype="Leave Encashment")) - frappe.get_doc(encashment).create_leave_ledger_entry() + frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry() def generate_expiry_allocation_ledger_entries(): ''' fix ledger entries for missing leave allocation transaction ''' @@ -65,24 +62,16 @@ def generate_expiry_allocation_ledger_entries(): for allocation in allocation_list: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}): - allocation.update(dict(doctype="Leave Allocation")) - allocation_obj = frappe.get_doc(allocation) + allocation_obj = frappe.get_doc("Leave Allocation", allocation) if allocation_obj.to_date <= getdate(today()): expire_allocation(allocation_obj) def get_allocation_records(): - return frappe.get_all("Leave Allocation", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated', - 'unused_leaves', 'from_date', 'to_date', 'carry_forward' - ], order_by='to_date ASC') + return frappe.get_all("Leave Allocation", filters={"docstatus": 1}, + fields=['name'], order_by='to_date ASC') def get_leaves_application_records(): - return frappe.get_all("Leave Application", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date']) + return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=['name']) def get_leave_encashment_records(): - return frappe.get_all("Leave Encashment", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date']) + return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=['name']) From c1719ef54b09fa1aa1633f003cb2daf2d216ac2c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 30 Oct 2020 19:19:48 +0530 Subject: [PATCH 27/45] fix: Place of Supply fix in Sales Invoices --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 88637bb4ec5..7bbb1d204e1 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -139,7 +139,7 @@ def get_place_of_supply(party_details, doctype): if not frappe.get_meta('Address').has_field('gst_state'): return if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): - address_name = party_details.shipping_address_name or party_details.customer_address + address_name = party_details.customer_address or party_details.shipping_address elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): address_name = party_details.shipping_address or party_details.supplier_address From 7036635007a5fc4bdbdcbf65167a270c93da1a59 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 30 Oct 2020 22:12:24 +0530 Subject: [PATCH 28/45] fix: fieldname --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 7bbb1d204e1..cb2f4c3429f 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -139,7 +139,7 @@ def get_place_of_supply(party_details, doctype): if not frappe.get_meta('Address').has_field('gst_state'): return if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): - address_name = party_details.customer_address or party_details.shipping_address + address_name = party_details.customer_address or party_details.shipping_address_name elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): address_name = party_details.shipping_address or party_details.supplier_address From 4f008f59fc54be84ced14cfe26c4a4f58c9df603 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Sep 2020 16:24:11 +0530 Subject: [PATCH 29/45] fix: Received/Delivered Items to Billed Logic --- .../delivered_items_to_be_billed.py | 18 ++++++++++++----- erpnext/accounts/report/non_billed_report.py | 20 +++++++++++++------ .../received_items_to_be_billed.py | 18 ++++++++++++----- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py index 3ffb3ac1df4..2aea3f64239 100644 --- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py @@ -14,11 +14,19 @@ def execute(filters=None): def get_column(): return [ - _("Delivery Note") + ":Link/Delivery Note:120", _("Status") + "::120", _("Date") + ":Date:100", - _("Suplier") + ":Link/Customer:120", _("Customer Name") + "::120", - _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Pending Amount") + ":Currency:100", - _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", + _("Delivery Note") + ":Link/Delivery Note:160", + _("Date") + ":Date:100", + _("Customer") + ":Link/Customer:120", + _("Customer Name") + "::120", + _("Item Code") + ":Link/Item:120", + _("Amount") + ":Currency:100", + _("Billed Amount") + ":Currency:100", + _("Returned Amount") + ":Currency:120", + _("Pending Amount") + ":Currency:100", + _("Item Name") + "::120", + _("Description") + "::120", + _("Project") + ":Link/Project:120", + _("Company") + ":Link/Company:120", ] def get_args(): diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py index a9e25bc25bf..2e18ce11ddc 100644 --- a/erpnext/accounts/report/non_billed_report.py +++ b/erpnext/accounts/report/non_billed_report.py @@ -17,18 +17,26 @@ def get_ordered_to_be_billed_data(args): return frappe.db.sql(""" Select - `{parent_tab}`.name, `{parent_tab}`.status, `{parent_tab}`.{date_field}, `{parent_tab}`.{party}, `{parent_tab}`.{party}_name, - {project_field}, `{child_tab}`.item_code, `{child_tab}`.base_amount, + `{parent_tab}`.name, `{parent_tab}`.{date_field}, + `{parent_tab}`.{party}, `{parent_tab}`.{party}_name, + `{child_tab}`.item_code, + `{child_tab}`.base_amount, (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)), - (`{child_tab}`.base_amount - (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1))), - `{child_tab}`.item_name, `{child_tab}`.description, `{parent_tab}`.company + (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)), + (`{child_tab}`.base_amount - + (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) - + (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))), + `{child_tab}`.item_name, `{child_tab}`.description, + {project_field}, `{parent_tab}`.company from `{parent_tab}`, `{child_tab}` where `{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1 and `{parent_tab}`.status not in ('Closed', 'Completed') - and `{child_tab}`.amount > 0 and round(`{child_tab}`.billed_amt * - ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) < `{child_tab}`.base_amount + and `{child_tab}`.amount > 0 + and (`{child_tab}`.base_amount - + round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) - + (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0 order by `{parent_tab}`.{order} {order_by} """.format(parent_tab = 'tab' + doctype, child_tab = 'tab' + child_tab, precision= precision, party = party, diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py index 5e8d7730b76..c7d4384a734 100644 --- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py +++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py @@ -14,11 +14,19 @@ def execute(filters=None): def get_column(): return [ - _("Purchase Receipt") + ":Link/Purchase Receipt:120", _("Status") + "::120", _("Date") + ":Date:100", - _("Supplier") + ":Link/Supplier:120", _("Supplier Name") + "::120", - _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Amount to Bill") + ":Currency:100", - _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", + _("Purchase Receipt") + ":Link/Purchase Receipt:160", + _("Date") + ":Date:100", + _("Supplier") + ":Link/Supplier:120", + _("Supplier Name") + "::120", + _("Item Code") + ":Link/Item:120", + _("Amount") + ":Currency:100", + _("Billed Amount") + ":Currency:100", + _("Returned Amount") + ":Currency:120", + _("Pending Amount") + ":Currency:120", + _("Item Name") + "::120", + _("Description") + "::120", + _("Project") + ":Link/Project:120", + _("Company") + ":Link/Company:120", ] def get_args(): From 2860f627721283569f9c6f94dd4731df606745dd Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 4 Nov 2020 14:15:26 +0530 Subject: [PATCH 30/45] Fix leave ledger patch (#23806) * fix: leave ledger patch * fix: modified patch date --- erpnext/patches.txt | 2 +- erpnext/patches/v12_0/generate_leave_ledger_entries.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1e704c86520..b5f31bafa7e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -635,7 +635,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') erpnext.patches.v12_0.add_default_dashboards erpnext.patches.v12_0.remove_bank_remittance_custom_fields -erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020 +erpnext.patches.v12_0.generate_leave_ledger_entries #04-11-2020 erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index 342c12996d1..7afde373c30 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -11,8 +11,6 @@ def execute(): frappe.reload_doc("HR", "doctype", "Leave Ledger Entry") frappe.reload_doc("HR", "doctype", "Leave Encashment") frappe.reload_doc("HR", "doctype", "Leave Type") - if frappe.db.a_row_exists("Leave Ledger Entry"): - return if not frappe.get_meta("Leave Allocation").has_field("unused_leaves"): frappe.reload_doc("HR", "doctype", "Leave Allocation") From 8e3731683223bfdde35c502b19238b93e0956ccf Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 4 Nov 2020 14:46:34 +0530 Subject: [PATCH 31/45] fix: list index out of range on incilding uom --- erpnext/stock/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index db39bae8a63..da4b529b01e 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -284,7 +284,6 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto return convertible_cols = {} - is_dict_obj = False if isinstance(result[0], dict): is_dict_obj = True @@ -306,13 +305,13 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto for row_idx, row in enumerate(result): data = row.items() if is_dict_obj else enumerate(row) for key, value in data: - if not key in convertible_columns or not conversion_factors[row_idx]: + if key not in convertible_columns or not conversion_factors[row_idx-1]: continue if convertible_columns.get(key) == 'rate': - new_value = flt(value) * conversion_factors[row_idx] + new_value = flt(value) * conversion_factors[row_idx-1] else: - new_value = flt(value) / conversion_factors[row_idx] + new_value = flt(value) / conversion_factors[row_idx-1] if not is_dict_obj: row.insert(key+1, new_value) From 7ee2b0ed3a872e94ee13a2b806d35a0dc742f23a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 4 Nov 2020 19:15:55 +0530 Subject: [PATCH 32/45] fix: default cost center in item master not set in stock entry --- .../doctype/work_order/test_work_order.py | 5 ++++ .../stock/doctype/stock_entry/stock_entry.py | 5 ++-- erpnext/stock/get_item_details.py | 23 ++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index d82a4dd9fe8..e562f99f39a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -371,6 +371,11 @@ class TestWorkOrder(unittest.TestCase): ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) self.assertEqual(len(ste1.items), 3) + def test_cost_center_for_manufacture(self): + wo_order = make_wo_order_test_record() + ste = make_stock_entry(wo_order.name, "Material Transfer for Manufacture", wo_order.qty) + self.assertEquals(ste.get("items")[0].get("cost_center"), "_Test Cost Center - _TC") + def test_operation_time_with_batch_size(self): fg_item = "Test Batch Size Item For BOM" rm1 = "Test Batch Size Item RM 1 For BOM" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3bed73e9258..5d01de9a9dd 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1194,8 +1194,6 @@ class StockEntry(StockController): return item_dict def add_to_stock_entry_detail(self, item_dict, bom_no=None): - cost_center = frappe.db.get_value("Company", self.company, 'cost_center') - for d in item_dict: stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") @@ -1206,9 +1204,10 @@ class StockEntry(StockController): se_child.uom = item_dict[d]["uom"] if item_dict[d].get("uom") else stock_uom se_child.stock_uom = stock_uom se_child.qty = flt(item_dict[d]["qty"], se_child.precision("qty")) - se_child.cost_center = item_dict[d].get("cost_center") or cost_center se_child.allow_alternative_item = item_dict[d].get("allow_alternative_item", 0) se_child.subcontracted_item = item_dict[d].get("main_item_code") + se_child.cost_center = (item_dict[d].get("cost_center") or + get_default_cost_center(item_dict[d], company = self.company)) for field in ["idx", "po_detail", "original_item", "expense_account", "description", "item_name"]: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index b1e38b340c5..48b4dc858c4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -527,23 +527,40 @@ def get_default_deferred_account(args, item, fieldname=None): else: return None -def get_default_cost_center(args, item, item_group, brand, company=None): +def get_default_cost_center(args, item=None, item_group=None, brand=None, company=None): cost_center = None + + if not company and args.get("company"): + company = args.get("company") + if args.get('project'): cost_center = frappe.db.get_value("Project", args.get("project"), "cost_center", cache=True) - if not cost_center: + if not cost_center and (item and item_group and brand): if args.get('customer'): cost_center = item.get('selling_cost_center') or item_group.get('selling_cost_center') or brand.get('selling_cost_center') else: cost_center = item.get('buying_cost_center') or item_group.get('buying_cost_center') or brand.get('buying_cost_center') - cost_center = cost_center or args.get("cost_center") + elif not cost_center and args.get("item_code") and company: + for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]: + path = "erpnext.stock.get_item_details.{0}".format(method) + data = frappe.get_attr(path)(args.get("item_code"), company) + + if data and (data.selling_cost_center or data.buying_cost_center): + return data.selling_cost_center or data.buying_cost_center + + if not cost_center and args.get("cost_center"): + cost_center = args.get("cost_center") if (company and cost_center and frappe.get_cached_value("Cost Center", cost_center, "company") != company): return None + if not cost_center and company: + cost_center = frappe.get_cached_value("Company", + company, "cost_center") + return cost_center def get_default_supplier(args, item, item_group, brand): From 7e6c79819aac158e17852f4d0f51899d40913a77 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 5 Nov 2020 11:59:45 +0530 Subject: [PATCH 33/45] fix: refactor and test --- erpnext/controllers/selling_controller.py | 40 +++++++------------ .../doctype/sales_order/test_sales_order.py | 1 + .../delivery_note/test_delivery_note.py | 33 +++++++++++++++ 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7496081b934..7147a7d5b2e 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -365,39 +365,29 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def set_po_nos(self): - self.po_no = '' if self.doctype == 'Sales Invoice' and hasattr(self, "items"): self.set_pos_for_sales_invoice() if self.doctype == 'Delivery Note' and hasattr(self, "items"): self.set_pos_for_delivery_note() def set_pos_for_sales_invoice(self): - ref_fieldname1 = "sales_order" - ref_fieldname2 = "delivery_note" - sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) - if sales_orders: - so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if so_po_nos and so_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) - delivery_notes = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) - if delivery_notes: - dn_po_nos = frappe.get_all('Delivery Note', 'po_no', filters = {'name': ('in', delivery_notes)}) - if dn_po_nos and dn_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in dn_po_nos if d.po_no]))) + po_nos = [] + self.get_po_nos('Sales Order', 'sales_order', po_nos) + self.get_po_nos('Delivery Note', 'delivery_note', po_nos) + self.po_no = ', '.join(list(set(po_nos))) def set_pos_for_delivery_note(self): - ref_fieldname1 = "against_sales_order" - ref_fieldname2 = "against_sales_invoice" - sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) - sales_invoices = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) - if sales_orders: - so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if so_po_nos and so_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) - if sales_invoices: - si_po_nos = frappe.get_all('Sales Invoice', 'po_no', filters = {'name': ('in', sales_invoices)}) - if si_po_nos and si_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in si_po_nos if d.po_no]))) + po_nos = [] + self.get_po_nos('Sales Order', 'against_sales_order', po_nos) + self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) + self.po_no = ', '.join(list(set(po_nos))) + + def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): + doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) + if doc_list: + po_no_list = frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) + if po_no_list and po_no_list[0].get('po_no'): + po_nos += [d.po_no for d in po_no_list if d.po_no] def set_gross_profit(self): if self.doctype == "Sales Order": diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index fcde0d502e9..6408d4d5124 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1050,6 +1050,7 @@ def make_sales_order(**args): so.company = args.company or "_Test Company" so.customer = args.customer or "_Test Customer" so.currency = args.currency or "INR" + so.po_no = args.po_no or '12345' if args.selling_price_list: so.selling_price_list = args.selling_price_list diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 9d92d43ec2f..a83741d3eaf 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -442,9 +442,15 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(dn.status, "To Bill") self.assertEqual(dn.per_billed, 0) + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, so.po_no) + si = make_sales_invoice(dn.name) si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, si.po_no) + dn.load_from_db() self.assertEqual(dn.get("items")[0].billed_amt, 200) self.assertEqual(dn.per_billed, 100) @@ -461,6 +467,9 @@ class TestDeliveryNote(unittest.TestCase): si.insert() si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, si.po_no) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) dn1 = make_delivery_note(so.name) @@ -469,6 +478,9 @@ class TestDeliveryNote(unittest.TestCase): dn1.get("items")[0].qty = 2 dn1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, dn1.po_no) + self.assertEqual(dn1.get("items")[0].billed_amt, 200) self.assertEqual(dn1.per_billed, 100) self.assertEqual(dn1.status, "Completed") @@ -479,6 +491,9 @@ class TestDeliveryNote(unittest.TestCase): dn2.get("items")[0].qty = 4 dn2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, dn2.po_no) + dn1.load_from_db() self.assertEqual(dn1.get("items")[0].billed_amt, 100) self.assertEqual(dn1.per_billed, 50) @@ -502,9 +517,15 @@ class TestDeliveryNote(unittest.TestCase): dn1.get("items")[0].qty = 2 dn1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn1.po_no, so.po_no) + si1 = make_sales_invoice(dn1.name) si1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn1.po_no, si1.po_no) + dn1.load_from_db() self.assertEqual(dn1.per_billed, 100) @@ -512,11 +533,17 @@ class TestDeliveryNote(unittest.TestCase): si2.get("items")[0].qty = 4 si2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(si2.po_no, so.po_no) + dn2 = make_delivery_note(so.name) dn2.posting_time = "08:00" dn2.get("items")[0].qty = 5 dn2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn2.po_no, so.po_no) + dn1.load_from_db() self.assertEqual(dn1.get("items")[0].billed_amt, 200) self.assertEqual(dn1.per_billed, 100) @@ -536,9 +563,15 @@ class TestDeliveryNote(unittest.TestCase): si = make_sales_invoice(so.name) si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, si.po_no) + dn = make_delivery_note(si.name) dn.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, si.po_no) + self.assertEqual(dn.get("items")[0].billed_amt, 1000) self.assertEqual(dn.per_billed, 100) self.assertEqual(dn.status, "Completed") From 06b6027674e088e03827d4811af3494abc7670fc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 5 Nov 2020 15:35:48 +0530 Subject: [PATCH 34/45] fix: not able to select assign to --- erpnext/assets/doctype/asset_maintenance/asset_maintenance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 8a954b94d1e..557246e7efc 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -108,7 +108,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_team_members(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }) + return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }, "team_member") @frappe.whitelist() def get_maintenance_log(asset_name): From 6beee63d06a6c8e819fe11005567cad897a98f11 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Nov 2020 16:29:34 +0530 Subject: [PATCH 35/45] fix: Auto State-wise gst tax template --- erpnext/regional/india/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 88637bb4ec5..45ee1729939 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -218,10 +218,9 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code): for tax_category in tax_categories: if tax_category.gst_state == number_state_mapping[state_code] or \ - (not default_tax and not tax_category.gst_state): + (not default_tax and not tax_category.gst_state): default_tax = frappe.db.get_value(master_doctype, - {'disabled': 0, 'tax_category': tax_category.name}, 'name') - + {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name') return default_tax def get_tax_template_for_sez(party_details, master_doctype, company, party_type): From 80f5734305166b78427f7e898727b174b40aac17 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 27 Oct 2020 13:18:30 +0530 Subject: [PATCH 36/45] fix: Payment Terms not fetched in Purchase Invoice from Purchase Receipt --- erpnext/accounts/party.py | 6 +++--- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 12e7b8b8c37..4e7922c6fbe 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -60,7 +60,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= billing_address=party_address, shipping_address=shipping_address) if fetch_payment_terms_template: - party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) + party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) if not party_details.get("currency"): party_details["currency"] = currency @@ -318,7 +318,7 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None): due_date = None if (bill_date or posting_date) and party: due_date = bill_date or posting_date - template_name = get_pyt_term_template(party, party_type, company) + template_name = get_payment_terms_template(party, party_type, company) if template_name: due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") @@ -425,7 +425,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup @frappe.whitelist() -def get_pyt_term_template(party_name, party_type, company=None): +def get_payment_terms_template(party_name, party_type, company=None): if party_type not in ("Customer", "Supplier"): return template = None diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8538b00fd6e..730a1d0c076 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -500,6 +500,8 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc + from erpnext.accounts.party import get_payment_terms_template + doc = frappe.get_doc('Purchase Receipt', source_name) returned_qty_map = get_returned_qty_map(source_name) invoiced_qty_map = get_invoiced_qty_map(source_name) @@ -510,6 +512,7 @@ def make_purchase_invoice(source_name, target_doc=None): doc = frappe.get_doc(target) doc.ignore_pricing_rule = 1 + doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) doc.run_method("onload") doc.run_method("set_missing_values") doc.run_method("calculate_taxes_and_totals") From f5e4f75fd0a8ab441db4dcdac432d82be33c2849 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 27 Oct 2020 14:49:43 +0530 Subject: [PATCH 37/45] chore: Test case for Payment Terms in PI from PR --- .../purchase_receipt/test_purchase_receipt.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d0208d01eda..26bcd457449 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -20,6 +20,30 @@ class TestPurchaseReceipt(unittest.TestCase): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) def test_make_purchase_invoice(self): + if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'): + frappe.get_doc({ + 'doctype': 'Payment Terms Template', + 'template_name': '_Test Payment Terms Template For Purchase Invoice', + 'allocate_payment_based_on_payment_terms': 1, + 'terms': [ + { + 'doctype': 'Payment Terms Template Detail', + 'invoice_portion': 50.00, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 00 + }, + { + 'doctype': 'Payment Terms Template Detail', + 'invoice_portion': 50.00, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 30 + }] + }).insert() + + template = frappe.db.get_value('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice') + old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms") + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template) + pr = make_purchase_receipt(do_not_save=True) self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name) pr.submit() @@ -29,10 +53,23 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pi.doctype, "Purchase Invoice") self.assertEqual(len(pi.get("items")), len(pr.get("items"))) - # modify rate + # test maintaining same rate throughout purchade cycle pi.get("items")[0].rate = 200 self.assertRaises(frappe.ValidationError, frappe.get_doc(pi).submit) + # test if payment terms are fetched and set in PI + self.assertEqual(pi.payment_terms_template, template) + self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[0].invoice_portion, 50) + self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[1].invoice_portion, 50) + + # teardown + pi.delete() # draft PI + pr.cancel() + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier) + frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete() + def test_purchase_receipt_no_gl_entry(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') From bd49da45e3278f5ea7aaa0ddf4d3ab427bf8a205 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Mon, 9 Nov 2020 20:21:25 +0530 Subject: [PATCH 38/45] feat: added column cost_center to receivable reports (#23837) * feat: added column cost_center to receivable reports * Update accounts_receivable.py Co-authored-by: Nabin Hait --- .../report/accounts_receivable/accounts_receivable.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f6632fa2632..08695e7f0d5 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -160,6 +160,8 @@ class ReceivablePayableReport(object): else: # advance / unlinked payment or other adjustment row.paid -= gle_balance + if gle.cost_center: + row.cost_center = gle.cost_center def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) @@ -210,7 +212,6 @@ class ReceivablePayableReport(object): for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 1.0/10 ** self.currency_precision: # non-zero oustanding, we must consider this row @@ -577,7 +578,7 @@ class ReceivablePayableReport(object): self.gl_entries = frappe.db.sql(""" select - name, posting_date, account, party_type, party, voucher_type, voucher_no, + name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, against_voucher_type, against_voucher, account_currency, remarks, {0} from `tabGL Entry` @@ -741,6 +742,7 @@ class ReceivablePayableReport(object): self.add_column(_("Customer Contact"), fieldname='customer_primary_contact', fieldtype='Link', options='Contact') + self.add_column(label=_('Cost Center'), fieldname='cost_center', fieldtype='Data') self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data') self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link', options='voucher_type', width=180) From cee0706a1fade44edb3b8c2d2d20efb4b997035c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 21 Oct 2020 11:04:00 +0530 Subject: [PATCH 39/45] fix: incorrect backflush qty in manufacture entry --- .../doctype/work_order/test_work_order.py | 36 +++++++++++++++++++ .../delivery_note/test_delivery_note.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 5 ++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index e562f99f39a..0263102bac0 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -193,6 +193,42 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(cint(bin1_on_end_production.projected_qty), cint(bin1_on_end_production.projected_qty)) + def test_backflush_qty_for_overpduction_manufacture(self): + cancel_stock_entry = [] + allow_overproduction("overproduction_percentage_for_work_order", 30) + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100) + ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", + target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0) + ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0) + + cancel_stock_entry.extend([ste1.name, ste2.name]) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 60)) + s.submit() + cancel_stock_entry.append(s.name) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 60)) + s.submit() + cancel_stock_entry.append(s.name) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 60)) + s.submit() + cancel_stock_entry.append(s.name) + + s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 50)) + s1.submit() + cancel_stock_entry.append(s1.name) + + self.assertEqual(s1.items[0].qty, 50) + self.assertEqual(s1.items[1].qty, 100) + cancel_stock_entry.reverse() + for ste in cancel_stock_entry: + doc = frappe.get_doc("Stock Entry", ste) + doc.cancel() + + allow_overproduction("overproduction_percentage_for_work_order", 0) + def test_reserved_qty_for_stopped_production(self): test_stock_entry.make_stock_entry(item_code="_Test Item", target= self.warehouse, qty=100, basic_rate=100) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 9d92d43ec2f..d368fcd20e2 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -57,7 +57,7 @@ class TestDeliveryNote(unittest.TestCase): sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name}) - self.assertEqual(sle.stock_value_difference, -1*stock_queue[0][1]) + self.assertEqual(sle.stock_value_difference, flt(-1*stock_queue[0][1])) self.assertFalse(get_gl_entries("Delivery Note", dn.name)) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5d01de9a9dd..1dd022fce00 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1113,7 +1113,10 @@ class StockEntry(StockController): for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse): if (qty > req_qty): - qty-= d.get(item.warehouse) + qty = (qty/trans_qty) * flt(self.fg_completed_qty) + + if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): + qty = frappe.utils.ceil(qty) if qty > 0: self.add_to_stock_entry_detail({ From 38e681e8b2d23bdcf5e36b14c4b45ffc38f404d0 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 11 Nov 2020 11:47:23 +0530 Subject: [PATCH 40/45] fix: refactor --- erpnext/controllers/selling_controller.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7147a7d5b2e..b2cc723ced5 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -374,20 +374,18 @@ class SellingController(StockController): po_nos = [] self.get_po_nos('Sales Order', 'sales_order', po_nos) self.get_po_nos('Delivery Note', 'delivery_note', po_nos) - self.po_no = ', '.join(list(set(po_nos))) + self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def set_pos_for_delivery_note(self): po_nos = [] self.get_po_nos('Sales Order', 'against_sales_order', po_nos) self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) - self.po_no = ', '.join(list(set(po_nos))) + self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) if doc_list: - po_no_list = frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) - if po_no_list and po_no_list[0].get('po_no'): - po_nos += [d.po_no for d in po_no_list if d.po_no] + po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')] def set_gross_profit(self): if self.doctype == "Sales Order": From 6b86586eb11f41f8b92e47501043aa6d272507d9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 9 Nov 2020 13:37:13 +0530 Subject: [PATCH 41/45] fix: incorrect outstanding amount for multicurrency with Reverse Charge --- erpnext/controllers/taxes_and_totals.py | 35 ++++++++++++++----------- erpnext/regional/india/utils.py | 8 +++--- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 28bfb7a0072..8fdda67f53b 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -531,16 +531,6 @@ class calculate_taxes_and_totals(object): self._set_in_company_currency(self.doc, ['write_off_amount']) if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: - grand_total = self.doc.rounded_total or self.doc.grand_total - if self.doc.party_account_currency == self.doc.currency: - total_amount_to_pay = flt(grand_total - self.doc.total_advance - - flt(self.doc.write_off_amount), self.doc.precision("grand_total")) - else: - total_amount_to_pay = flt(flt(grand_total * - self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance - - flt(self.doc.base_write_off_amount), self.doc.precision("grand_total")) - - self.doc.round_floats_in(self.doc, ["paid_amount"]) change_amount = 0 if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'): @@ -549,14 +539,10 @@ class calculate_taxes_and_totals(object): change_amount = self.doc.change_amount \ if self.doc.party_account_currency == self.doc.currency else self.doc.base_change_amount - paid_amount = self.doc.paid_amount \ - if self.doc.party_account_currency == self.doc.currency else self.doc.base_paid_amount - - self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount), - self.doc.precision("outstanding_amount")) + calculate_outstanding_amount(self.doc, change_amount) if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): - self.update_paid_amount_for_return(total_amount_to_pay) + self.update_paid_amount_for_return(self.doc.total_amount_to_pay) def calculate_paid_amount(self): @@ -751,3 +737,20 @@ def get_rounded_tax_amount(itemised_tax, precision): for taxes in itemised_tax.values(): for tax_account in taxes: taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) + +def calculate_outstanding_amount(doc, change_amount=None): + grand_total = doc.rounded_total or doc.grand_total + if doc.party_account_currency == doc.currency: + doc.total_amount_to_pay = flt(grand_total - doc.total_advance + - flt(doc.write_off_amount), doc.precision("grand_total")) + else: + doc.total_amount_to_pay = flt(flt(grand_total * + doc.conversion_rate, doc.precision("grand_total")) - doc.total_advance + - flt(doc.base_write_off_amount), doc.precision("grand_total")) + + doc.round_floats_in(doc, ["paid_amount"]) + paid_amount = doc.paid_amount \ + if doc.party_account_currency == doc.currency else doc.base_paid_amount + + doc.outstanding_amount = flt(doc.total_amount_to_pay - flt(paid_amount) + flt(change_amount), + doc.precision("outstanding_amount")) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index ea6a2cd2227..bc182382802 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -4,7 +4,7 @@ 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 +from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount, calculate_outstanding_amount from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.hr.utils import get_salary_assignment from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip @@ -689,16 +689,14 @@ def update_totals(gst_tax, base_gst_tax, doc): doc.grand_total -= gst_tax if doc.meta.get_field("rounded_total"): - if doc.is_rounded_total_disabled(): - doc.outstanding_amount = doc.grand_total - else: + if not doc.is_rounded_total_disabled(): doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total, doc.currency, doc.precision("rounded_total")) doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total, doc.precision("rounding_adjustment")) - doc.outstanding_amount = doc.rounded_total or doc.grand_total + calculate_outstanding_amount(doc) 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)) From 16427c5cc84cc21a3b6e87cf19dc5b58f7b075e9 Mon Sep 17 00:00:00 2001 From: Marica Date: Fri, 23 Oct 2020 19:40:55 +0530 Subject: [PATCH 42/45] fix: SO to PO flow improvement (#23357) * fix: SO to PO flow improvement * fix: Dont map shipping_address - shipping_address is a text field in SO and link field in PO - Drop shipping case handles its mapping - normal case doesnt need to map * fix: Hide/Add rows depending on Against Default Supplier * fix: Removed Default Supplier Select field from popup - removed Default Supplier Select field from popup - only loop through suppliers of selected items if via default supplier - only check for items in selected items * fix: Sales Order Drop Shipping Test * fix: (translation)Multi line to single line strings Co-authored-by: Nabin Hait --- .../doctype/sales_order/sales_order.js | 161 +++++++++++------- .../doctype/sales_order/sales_order.py | 140 ++++++++------- .../doctype/sales_order/test_sales_order.py | 89 ++++------ 3 files changed, 216 insertions(+), 174 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 423922e4865..59520c3cb22 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -148,7 +148,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( // sales invoice if(flt(doc.per_billed, 6) < 100) { - this.frm.add_custom_button(__('Invoice'), () => me.make_sales_invoice(), __('Create')); + this.frm.add_custom_button(__('Sales Invoice'), () => me.make_sales_invoice(), __('Create')); } // material request @@ -542,19 +542,32 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( }, make_purchase_order: function(){ + let pending_items = this.frm.doc.items.some((item) =>{ + let pending_qty = flt(item.stock_qty) - flt(item.ordered_qty); + return pending_qty > 0; + }) + if(!pending_items){ + frappe.throw({message: __("Purchase Order already created for all Sales Order items"), title: __("Note")}); + } + var me = this; var dialog = new frappe.ui.Dialog({ - title: __("For Supplier"), + title: __("Select Items"), fields: [ - {"fieldtype": "Link", "label": __("Supplier"), "fieldname": "supplier", "options":"Supplier", - "description": __("Leave the field empty to make purchase orders for all suppliers"), - "get_query": function () { - return { - query:"erpnext.selling.doctype.sales_order.sales_order.get_supplier", - filters: {'parent': me.frm.doc.name} - } - }}, - {fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items', + { + "fieldtype": "Check", + "label": __("Against Default Supplier"), + "fieldname": "against_default_supplier", + "default": 0 + }, + { + "fieldtype": "Section Break", + "label": "", + "fieldname": "sec_break_dialog", + "hide_border": 1 + }, + { + fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items', fields: [ { fieldtype:'Data', @@ -572,8 +585,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( }, { fieldtype:'Float', - fieldname:'qty', - label: __('Quantity'), + fieldname:'pending_qty', + label: __('Pending Qty'), read_only: 1, in_list_view:1 }, @@ -582,60 +595,86 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( read_only:1, fieldname:'uom', label: __('UOM'), + in_list_view:1, + }, + { + fieldtype:'Data', + fieldname:'supplier', + label: __('Supplier'), + read_only:1, in_list_view:1 - } + }, ], - data: cur_frm.doc.items, - get_data: function() { - return cur_frm.doc.items - } - }, - - {"fieldtype": "Button", "label": __('Create Purchase Order'), "fieldname": "make_purchase_order", "cssClass": "btn-primary"}, - ] - }); - - dialog.fields_dict.make_purchase_order.$input.click(function() { - var args = dialog.get_values(); - let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children() - if(selected_items.length == 0) { - frappe.throw({message: 'Please select Item form Table', title: __('Message'), indicator:'blue'}) - } - let selected_items_list = [] - for(let i in selected_items){ - selected_items_list.push(selected_items[i].item_code) - } - dialog.hide(); - return frappe.call({ - type: "GET", - method: "erpnext.selling.doctype.sales_order.sales_order.make_purchase_order", - args: { - "source_name": me.frm.doc.name, - "for_supplier": args.supplier, - "selected_items": selected_items_list - }, - freeze: true, - callback: function(r) { - if(!r.exc) { - // var args = dialog.get_values(); - if (args.supplier){ - var doc = frappe.model.sync(r.message); - frappe.set_route("Form", r.message.doctype, r.message.name); - } - else{ - frappe.route_options = { - "sales_order": me.frm.doc.name - } - frappe.set_route("List", "Purchase Order"); - } - } + data: me.frm.doc.items.map((item) =>{ + item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); + return item; + }).filter((item) => {return item.pending_qty > 0;}) } - }) + ], + primary_action_label: 'Create Purchase Order', + primary_action (args) { + if (!args) return; + let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children(); + if(selected_items.length == 0) { + frappe.throw({message: 'Please select Items from the Table', title: __('Items Required'), indicator:'blue'}) + } + + dialog.hide(); + + var method = args.against_default_supplier ? "make_purchase_order_for_default_supplier" : "make_purchase_order" + return frappe.call({ + type: "GET", + method: "erpnext.selling.doctype.sales_order.sales_order." + method, + args: { + "source_name": me.frm.doc.name, + "selected_items": selected_items + }, + freeze: true, + callback: function(r) { + if(!r.exc) { + if (!args.against_default_supplier) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + else { + frappe.route_options = { + "sales_order": me.frm.doc.name + } + frappe.set_route("List", "Purchase Order"); + } + } + } + }) + } }); - dialog.get_field("items_for_po").grid.only_sortable() - dialog.get_field("items_for_po").refresh() + + dialog.fields_dict["against_default_supplier"].df.onchange = () => { + console.log("yo"); + var against_default_supplier = dialog.get_value("against_default_supplier"); + var items_for_po = dialog.get_value("items_for_po"); + + if (against_default_supplier) { + let items_with_supplier = items_for_po.filter((item) => item.supplier) + + dialog.fields_dict["items_for_po"].df.data = items_with_supplier; + dialog.get_field("items_for_po").refresh(); + } else { + let pending_items = me.frm.doc.items.map((item) =>{ + item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); + return item; + }).filter((item) => {return item.pending_qty > 0;}); + + dialog.fields_dict["items_for_po"].df.data = pending_items; + dialog.get_field("items_for_po").refresh(); + } + } + + dialog.get_field("items_for_po").grid.only_sortable(); + dialog.get_field("items_for_po").refresh(); + dialog.wrapper.find('.grid-heading-row .grid-row-check').click(); dialog.show(); }, + hold_sales_order: function(){ var me = this; var d = new frappe.ui.Dialog({ diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ed3a96446a0..554e467039b 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -443,25 +443,19 @@ class SalesOrder(SellingController): for item in self.items: if item.ensure_delivery_based_on_produced_serial_no: if item.item_code in normal_items: - frappe.throw(_("Cannot ensure delivery by Serial No as \ - Item {0} is added with and without Ensure Delivery by \ - Serial No.").format(item.item_code)) + frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) if item.item_code not in reserved_items: if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): - frappe.throw(_("Item {0} has no Serial No. Only serilialized items \ - can have delivery based on Serial No").format(item.item_code)) + frappe.throw(_("Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No").format(item.item_code)) if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}): - frappe.throw(_("No active BOM found for item {0}. Delivery by \ - Serial No cannot be ensured").format(item.item_code)) + frappe.throw(_("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format(item.item_code)) reserved_items.append(item.item_code) else: normal_items.append(item.item_code) if not item.ensure_delivery_based_on_produced_serial_no and \ item.item_code in reserved_items: - frappe.throw(_("Cannot ensure delivery by Serial No as \ - Item {0} is added with and without Ensure Delivery by \ - Serial No.").format(item.item_code)) + frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context @@ -785,7 +779,7 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_purchase_order(source_name, for_supplier=None, selected_items=[], target_doc=None): +def make_purchase_order_for_default_supplier(source_name, selected_items=[], target_doc=None): if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) @@ -822,24 +816,21 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe def update_item(source, target, source_parent): target.schedule_date = source.delivery_date - target.qty = flt(source.qty) - flt(source.ordered_qty) - target.stock_qty = (flt(source.qty) - flt(source.ordered_qty)) * flt(source.conversion_factor) + target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) + target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) target.project = source_parent.project - suppliers =[] - if for_supplier: - suppliers.append(for_supplier) - else: - sales_order = frappe.get_doc("Sales Order", source_name) - for item in sales_order.items: - if item.supplier and item.supplier not in suppliers: - suppliers.append(item.supplier) + suppliers = [item.get('supplier') for item in selected_items if item.get('supplier') and item.get('supplier')] + suppliers = list(set(suppliers)) + + items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')] + items_to_map = list(set(items_to_map)) if not suppliers: frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) for supplier in suppliers: - po =frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) + po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) if len(po) == 0: doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { @@ -850,7 +841,8 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe "contact_mobile", "contact_email", "contact_person", - "taxes_and_charges" + "taxes_and_charges", + "shipping_address" ], "validation": { "docstatus": ["=", 1] @@ -872,52 +864,82 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe "item_tax_template" ], "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.qty and doc.supplier == supplier and doc.item_code in selected_items + "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map } }, target_doc, set_missing_values) - if not for_supplier: - doc.insert() + + doc.insert() else: suppliers =[] if suppliers: - if not for_supplier: - frappe.db.commit() + frappe.db.commit() return doc else: - frappe.msgprint(_("PO already created for all sales order items")) - + frappe.msgprint(_("Purchase Order already created for all Sales Order items")) @frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def get_supplier(doctype, txt, searchfield, start, page_len, filters): - supp_master_name = frappe.defaults.get_user_default("supp_master_name") - if supp_master_name == "Supplier Name": - fields = ["name", "supplier_group"] - else: - fields = ["name", "supplier_name", "supplier_group"] - fields = ", ".join(fields) +def make_purchase_order(source_name, selected_items=[], target_doc=None): + if isinstance(selected_items, string_types): + selected_items = json.loads(selected_items) - return frappe.db.sql("""select {field} from `tabSupplier` - where docstatus < 2 - and ({key} like %(txt)s - or supplier_name like %(txt)s) - and name in (select supplier from `tabSales Order Item` where parent = %(parent)s) - and name not in (select supplier from `tabPurchase Order` po inner join `tabPurchase Order Item` poi - on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s) - order by - if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), - if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999), - name, supplier_name - limit %(start)s, %(page_len)s """.format(**{ - 'field': fields, - 'key': frappe.db.escape(searchfield) - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len, - 'parent': filters.get('parent') - }) + items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')] + items_to_map = list(set(items_to_map)) + + def set_missing_values(source, target): + target.supplier = "" + target.apply_discount_on = "" + target.additional_discount_percentage = 0.0 + target.discount_amount = 0.0 + target.inter_company_order_reference = "" + target.customer = "" + target.customer_name = "" + target.run_method("set_missing_values") + target.run_method("calculate_taxes_and_totals") + + def update_item(source, target, source_parent): + target.schedule_date = source.delivery_date + target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) + target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) + target.project = source_parent.project + + # po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) + doc = get_mapped_doc("Sales Order", source_name, { + "Sales Order": { + "doctype": "Purchase Order", + "field_no_map": [ + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "taxes_and_charges", + "shipping_address" + ], + "validation": { + "docstatus": ["=", 1] + } + }, + "Sales Order Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_item"], + ["parent", "sales_order"], + ["stock_uom", "stock_uom"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["delivery_date", "schedule_date"] + ], + "field_no_map": [ + "rate", + "price_list_rate", + "item_tax_template", + "supplier" + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map + } + }, target_doc, set_missing_values) + return doc @frappe.whitelist() def make_work_orders(items, sales_order, company, project=None): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 6408d4d5124..37c29befcd7 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -674,12 +674,12 @@ class TestSalesOrder(unittest.TestCase): frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) def test_drop_shipping(self): - from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \ + update_status as so_update_status from erpnext.buying.doctype.purchase_order.purchase_order import update_status - make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) + # make items po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1}) - dn_item = make_item("_Test Regular Item", {"is_stock_item": 1}) so_items = [ @@ -701,80 +701,61 @@ class TestSalesOrder(unittest.TestCase): ] if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item")==1: - make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=10, rate=100) + make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=2, rate=100) - #setuo existing qty from bin - bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) - - existing_ordered_qty = bin[0].ordered_qty if bin else 0.0 - existing_reserved_qty = bin[0].reserved_qty if bin else 0.0 - - bin = frappe.get_all("Bin", filters={"item_code": dn_item.item_code, - "warehouse": "_Test Warehouse - _TC"}, fields=["reserved_qty"]) - - existing_reserved_qty_for_dn_item = bin[0].reserved_qty if bin else 0.0 - - #create so, po and partial dn + #create so, po and dn so = make_sales_order(item_list=so_items, do_not_submit=True) so.submit() - po = make_purchase_order(so.name, '_Test Supplier', selected_items=[so_items[0]['item_code']]) + po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) po.submit() - dn = create_dn_against_so(so.name, delivered_qty=1) + dn = create_dn_against_so(so.name, delivered_qty=2) self.assertEqual(so.customer, po.customer) self.assertEqual(po.items[0].sales_order, so.name) self.assertEqual(po.items[0].item_code, po_item.item_code) self.assertEqual(dn.items[0].item_code, dn_item.item_code) - - #test ordered_qty and reserved_qty - bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) - - ordered_qty = bin[0].ordered_qty if bin else 0.0 - reserved_qty = bin[0].reserved_qty if bin else 0.0 - - self.assertEqual(abs(flt(ordered_qty)), existing_ordered_qty) - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty) - - reserved_qty = frappe.db.get_value("Bin", - {"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty") - - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item + 1) - #test po_item length self.assertEqual(len(po.items), 1) - #test per_delivered status + # test ordered_qty and reserved_qty for drop ship item + bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + fields=["ordered_qty", "reserved_qty"]) + + ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 + reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 + + # drop ship PO should not impact bin, test the same + self.assertEqual(abs(flt(ordered_qty)), 0) + self.assertEqual(abs(flt(reserved_qty)), 0) + + # test per_delivered status update_status("Delivered", po.name) - self.assertEqual(flt(frappe.db.get_value("Sales Order", so.name, "per_delivered"), 2), 75.00) + self.assertEqual(flt(frappe.db.get_value("Sales Order", so.name, "per_delivered"), 2), 100.00) + po.load_from_db() - #test reserved qty after complete delivery - dn = create_dn_against_so(so.name, delivered_qty=1) - reserved_qty = frappe.db.get_value("Bin", - {"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty") - - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item) - - #test after closing so + # test after closing so so.db_set('status', "Closed") so.update_reserved_qty() - bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + # test ordered_qty and reserved_qty for drop ship item after closing so + bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, fields=["ordered_qty", "reserved_qty"]) - ordered_qty = bin[0].ordered_qty if bin else 0.0 - reserved_qty = bin[0].reserved_qty if bin else 0.0 + ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 + reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 - self.assertEqual(abs(flt(ordered_qty)), existing_ordered_qty) - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty) + self.assertEqual(abs(flt(ordered_qty)), 0) + self.assertEqual(abs(flt(reserved_qty)), 0) - reserved_qty = frappe.db.get_value("Bin", - {"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty") - - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item) + # teardown + so_update_status("Draft", so.name) + dn.load_from_db() + dn.cancel() + po.cancel() + so.load_from_db() + so.cancel() def test_reserved_qty_for_closing_so(self): bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, From 40ba013636f692054bfa66edc93d61af8a754876 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 4 Nov 2020 18:23:00 +0530 Subject: [PATCH 43/45] fix: SO to PO Mapping Issue - removed type: GET , which made the URL longer - Added only relevant fields from Items table to Dialog table to reduce args load - Made separate method to set table data in dialog - Added freeze message --- .../doctype/sales_order/sales_order.js | 42 ++++++++++--------- .../doctype/sales_order/sales_order.py | 8 +++- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 59520c3cb22..5f1b5934b00 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -560,12 +560,6 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( "fieldname": "against_default_supplier", "default": 0 }, - { - "fieldtype": "Section Break", - "label": "", - "fieldname": "sec_break_dialog", - "hide_border": 1 - }, { fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items', fields: [ @@ -604,16 +598,13 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( read_only:1, in_list_view:1 }, - ], - data: me.frm.doc.items.map((item) =>{ - item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); - return item; - }).filter((item) => {return item.pending_qty > 0;}) + ] } ], primary_action_label: 'Create Purchase Order', primary_action (args) { if (!args) return; + let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children(); if(selected_items.length == 0) { frappe.throw({message: 'Please select Items from the Table', title: __('Items Required'), indicator:'blue'}) @@ -623,8 +614,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( var method = args.against_default_supplier ? "make_purchase_order_for_default_supplier" : "make_purchase_order" return frappe.call({ - type: "GET", method: "erpnext.selling.doctype.sales_order.sales_order." + method, + freeze: true, + freeze_message: __("Creating Purchase Order ..."), args: { "source_name": me.frm.doc.name, "selected_items": selected_items @@ -648,8 +640,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } }); - dialog.fields_dict["against_default_supplier"].df.onchange = () => { - console.log("yo"); + dialog.fields_dict["against_default_supplier"].df.onchange = () => set_po_items_data(dialog); + + function set_po_items_data (dialog) { var against_default_supplier = dialog.get_value("against_default_supplier"); var items_for_po = dialog.get_value("items_for_po"); @@ -659,16 +652,27 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( dialog.fields_dict["items_for_po"].df.data = items_with_supplier; dialog.get_field("items_for_po").refresh(); } else { - let pending_items = me.frm.doc.items.map((item) =>{ - item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); - return item; - }).filter((item) => {return item.pending_qty > 0;}); + let po_items = []; + me.frm.doc.items.forEach(d => { + let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); + if (pending_qty > 0) { + po_items.push({ + "name": d.name, + "item_name": d.item_name, + "item_code": d.item_code, + "pending_qty": pending_qty, + "uom": d.uom, + "supplier": d.supplier + }); + } + }); - dialog.fields_dict["items_for_po"].df.data = pending_items; + dialog.fields_dict["items_for_po"].df.data = po_items; dialog.get_field("items_for_po").refresh(); } } + set_po_items_data(dialog); dialog.get_field("items_for_po").grid.only_sortable(); dialog.get_field("items_for_po").refresh(); dialog.wrapper.find('.grid-heading-row .grid-row-check').click(); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 554e467039b..dbdf2ea0cc6 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -779,7 +779,9 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_purchase_order_for_default_supplier(source_name, selected_items=[], target_doc=None): +def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): + if not selected_items: return + if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) @@ -878,7 +880,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar frappe.msgprint(_("Purchase Order already created for all Sales Order items")) @frappe.whitelist() -def make_purchase_order(source_name, selected_items=[], target_doc=None): +def make_purchase_order(source_name, selected_items=None, target_doc=None): + if not selected_items: return + if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) From 09e1b5314b25223acdc7643819bd85b0ca42ab62 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 4 Nov 2020 21:00:52 +0530 Subject: [PATCH 44/45] fix: Make sure row object has doctype so that read only property is applied to rows --- erpnext/selling/doctype/sales_order/sales_order.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 5f1b5934b00..6a9e43e273a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -657,6 +657,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); if (pending_qty > 0) { po_items.push({ + "doctype": "Sales Order Item", "name": d.name, "item_name": d.item_name, "item_code": d.item_code, From e1b96d70db5bd652a5ba1d2a58748006579cbdee Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 15 Nov 2020 16:42:55 +0530 Subject: [PATCH 45/45] refactor: show form buttons only if permissions exist (#23889) * refactor: show form buttons only if permissions exist * fix: missing semicolon Co-authored-by: Nabin Hait --- erpnext/setup/doctype/company/company.js | 48 +++++++++++++++--------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 14cacf37bb6..6305a7a925d 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -71,29 +71,41 @@ frappe.ui.form.on("Company", { frm.toggle_enable("default_currency", (frm.doc.__onload && !frm.doc.__onload.transactions_exist)); - frm.add_custom_button(__('Create Tax Template'), function() { - frm.trigger("make_default_tax_template"); - }); + if (frm.has_perm('write')) { + frm.add_custom_button(__('Create Tax Template'), function() { + frm.trigger("make_default_tax_template"); + }); + } + if (frappe.perm.has_perm("Cost Center", 0, 'read')) { + frm.add_custom_button(__('Cost Centers'), function() { + frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Cost Centers'), function() { - frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}) - }, __("View")); + if (frappe.perm.has_perm("Account", 0, 'read')) { + frm.add_custom_button(__('Chart of Accounts'), function() { + frappe.set_route('Tree', 'Account', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Chart of Accounts'), function() { - frappe.set_route('Tree', 'Account', {'company': frm.doc.name}) - }, __("View")); - frm.add_custom_button(__('Sales Tax Template'), function() { - frappe.set_route('List', 'Sales Taxes and Charges Template', {'company': frm.doc.name}); - }, __("View")); + if (frappe.perm.has_perm("Sales Taxes and Charges Template", 0, 'read')) { + frm.add_custom_button(__('Sales Tax Template'), function() { + frappe.set_route('List', 'Sales Taxes and Charges Template', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Purchase Tax Template'), function() { - frappe.set_route('List', 'Purchase Taxes and Charges Template', {'company': frm.doc.name}); - }, __("View")); + if (frappe.perm.has_perm("Purchase Taxes and Charges Template", 0, 'read')) { + frm.add_custom_button(__('Purchase Tax Template'), function() { + frappe.set_route('List', 'Purchase Taxes and Charges Template', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Default Tax Template'), function() { - frm.trigger("make_default_tax_template"); - }, __('Create')); + if (frm.has_perm('write')) { + frm.add_custom_button(__('Default Tax Template'), function() { + frm.trigger("make_default_tax_template"); + }, __('Create')); + } } erpnext.company.set_chart_of_accounts_options(frm.doc);