diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 7c8ae6cfb8..0cf3d24478 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_child_uom_conv_factor_change(self): po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py index b31b757a37..296473efe1 100644 --- a/erpnext/communication/doctype/call_log/call_log.py +++ b/erpnext/communication/doctype/call_log/call_log.py @@ -15,9 +15,9 @@ class CallLog(Document): number = strip_number(self.get('from')) self.contact = get_contact_with_phone_number(number) self.lead = get_lead_with_phone_number(number) - - contact = frappe.get_doc("Contact", self.contact) - self.customer = contact.get_link_for("Customer") + if self.contact: + contact = frappe.get_doc("Contact", self.contact) + self.customer = contact.get_link_for("Customer") def after_insert(self): self.trigger_call_popup() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 166564961d..fc32977658 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -605,8 +605,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) @@ -1170,6 +1168,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 @@ -1185,6 +1208,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor 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.") @@ -1209,6 +1233,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/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 394883d239..f743d707f7 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -52,7 +52,7 @@ class StockController(AccountsController): frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}") .format(d.idx, serial_no_data.name, d.batch_no)) - if d.qty > 0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: + if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") if expiry_date and getdate(expiry_date) < getdate(self.posting_date): diff --git a/erpnext/erpnext_integrations/desk_page/erpnext_integrations_settings/erpnext_integrations_settings.json b/erpnext/erpnext_integrations/desk_page/erpnext_integrations_settings/erpnext_integrations_settings.json new file mode 100644 index 0000000000..3bbc36ad94 --- /dev/null +++ b/erpnext/erpnext_integrations/desk_page/erpnext_integrations_settings/erpnext_integrations_settings.json @@ -0,0 +1,30 @@ +{ + "cards": [ + { + "hidden": 0, + "label": "Integrations Settings", + "links": "[\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Woocommerce Settings\"\n\t},\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Shopify Settings\",\n\t\t\"description\": \"Connect Shopify with ERPNext\"\n\t},\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Amazon MWS Settings\",\n\t\t\"description\": \"Connect Amazon with ERPNext\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Plaid Settings\",\n\t\t\"description\": \"Connect your bank accounts to ERPNext\"\n\t},\n {\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Exotel Settings\",\n\t\t\"description\": \"Connect your Exotel Account to ERPNext and track call logs\"\n }\n]" + } + ], + "category": "Modules", + "charts": [], + "creation": "2020-07-31 10:38:54.021237", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Desk Page", + "extends": "Settings", + "extends_another_page": 1, + "hide_custom": 0, + "idx": 0, + "is_standard": 1, + "label": "ERPNext Integrations Settings", + "modified": "2020-07-31 10:44:39.374297", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "ERPNext Integrations Settings", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [] +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index a244f582c4..9ce465ccaf 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -636,7 +636,7 @@ erpnext.work_order = { description: __('Max: {0}', [max]), default: max }, data => { - max += (max * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; + max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; if (data.qty > max) { frappe.msgprint(__('Quantity must not be more than {0}', [max])); diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 1ce36dd8bf..2f5f979bdf 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -403,7 +403,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 @@ -437,7 +437,7 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) test_user.remove_roles("Accounts User") frappe.set_user("Administrator") - + def test_update_child_qty_rate_with_workflow(self): from frappe.model.workflow import apply_workflow @@ -506,6 +506,95 @@ class TestSalesOrder(unittest.TestCase): so.reload() self.assertEqual(so.packed_items[0].qty, 8) + 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/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a92d04ff8c..2a541697bb 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -96,7 +96,7 @@ class StockEntry(StockController): self.update_quality_inspection() if self.work_order and self.purpose == "Manufacture": self.update_so_in_serial_number() - + if self.purpose == 'Material Transfer' and self.add_to_transit: self.set_material_request_transfer_status('In Transit') if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: @@ -205,7 +205,9 @@ class StockEntry(StockController): for f in ("uom", "stock_uom", "description", "item_name", "expense_account", "cost_center", "conversion_factor"): - if f in ["stock_uom", "conversion_factor"] or not item.get(f): + if f == "stock_uom" or not item.get(f): + item.set(f, item_details.get(f)) + if f == 'conversion_factor' and item.uom == item_details.get('stock_uom'): item.set(f, item_details.get(f)) if not item.transfer_qty and item.qty: @@ -849,6 +851,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: @@ -865,14 +869,16 @@ 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 backflush_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 backflush_based_on== "BOM" and - (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") + 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: frappe.throw(_("Manufacturing Quantity is mandatory")) @@ -1111,7 +1117,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: @@ -1137,12 +1142,24 @@ class StockEntry(StockController): item_dict = self.get_pro_order_required_items(backflush_based_on) 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 or - (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture")): + if (desire_to_transfer <= pending_to_issue + or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture") + or allow_overproduction): item_dict[item]["qty"] = desire_to_transfer elif pending_to_issue > 0: item_dict[item]["qty"] = pending_to_issue @@ -1370,7 +1387,7 @@ class StockEntry(StockController): if self.outgoing_stock_entry: parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, 'add_to_transit') - for item in self.items: + for item in self.items: material_request = item.material_request or None if self.purpose == "Material Transfer" and material_request not in material_requests: if self.outgoing_stock_entry and parent_se: @@ -1430,7 +1447,7 @@ def make_stock_in_entry(source_name, target_doc=None): if add_to_transit: warehouse = frappe.get_value('Material Request Item', source_doc.material_request_item, 'warehouse') target_doc.t_warehouse = warehouse - + target_doc.s_warehouse = source_doc.t_warehouse target_doc.qty = source_doc.qty - source_doc.transferred_qty diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index d98870de3e..9b6744ca3c 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -795,6 +795,32 @@ class TestStockEntry(unittest.TestCase): ]) ) + def test_conversion_factor_change(self): + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) + repack_entry = frappe.copy_doc(test_records[3]) + repack_entry.posting_date = nowdate() + repack_entry.posting_time = nowtime() + repack_entry.set_stock_entry_type() + repack_entry.insert() + + # check current uom and conversion factor + self.assertTrue(repack_entry.items[0].uom, "_Test UOM") + self.assertTrue(repack_entry.items[0].conversion_factor, 1) + + # change conversion factor + repack_entry.items[0].uom = "_Test UOM 1" + repack_entry.items[0].stock_uom = "_Test UOM 1" + repack_entry.items[0].conversion_factor = 2 + repack_entry.save() + repack_entry.submit() + + self.assertEqual(repack_entry.items[0].conversion_factor, 2) + self.assertEqual(repack_entry.items[0].uom, "_Test UOM 1") + self.assertEqual(repack_entry.items[0].qty, 50) + self.assertEqual(repack_entry.items[0].transfer_qty, 100) + + frappe.db.set_default("allow_negative_stock", 0) + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 7b9c129804..ae2e3a134f 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -238,7 +238,6 @@ "oldfieldname": "conversion_factor", "oldfieldtype": "Currency", "print_hide": 1, - "read_only": 1, "reqd": 1 }, { @@ -498,15 +497,14 @@ "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", "fieldname": "set_basic_rate_manually", "fieldtype": "Check", - "label": "Set Basic Rate Manually", - "show_days": 1, - "show_seconds": 1 + "label": "Set Basic Rate Manually" } ], "idx": 1, + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-06-08 12:57:03.172887", + "modified": "2020-09-22 17:55:03.384138", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 1a7c15ebca..8d8dcb74c3 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -398,6 +398,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):