From 614559f234411b8d4b558f506eadd9aa51ab9c39 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 13 Dec 2018 16:36:35 +0100 Subject: [PATCH 01/76] fix(pro-plan): Actual Qty of RM considering children in case source warehouse is a group warehouse --- .../manufacturing/doctype/production_plan/production_plan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 7d11ae4993..d4620cd952 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -499,7 +499,8 @@ def get_bin_details(row): conditions = "" warehouse = row.source_warehouse or row.default_warehouse or row.warehouse if warehouse: - conditions = " and warehouse='{0}'".format(frappe.db.escape(warehouse)) + lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) + conditions = " and exists(select name from `tabWarehouse` where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse)".format(lft, rgt) item_projected_qty = frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, ifnull(sum(actual_qty),0) as actual_qty from `tabBin` From 52a692ee084e9614d430a4e5425f2ad625ee7950 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 26 Dec 2018 00:40:17 +0530 Subject: [PATCH 02/76] [Fix] Not able to delete customer if contact is available --- erpnext/selling/doctype/customer/customer.py | 5 +++++ erpnext/selling/doctype/customer/test_customer.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index b17345c2e7..2a6853aa67 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -173,6 +173,11 @@ class Customer(TransactionBase): frappe.throw(_("""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""").format(outstanding_amt)) def on_trash(self): + if self.customer_primary_contact: + frappe.db.sql("""update `tabCustomer` + set customer_primary_contact=null, mobile_no=null, email_id=null + where name=%s""", self.name) + delete_contact_and_address('Customer', self.name) if self.lead_name: frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 45546e348a..123fd552c6 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -96,6 +96,15 @@ class TestCustomer(unittest.TestCase): so.save() + def test_delete_customer_contact(self): + customer = frappe.get_doc( + get_customer_dict('_Test Customer for delete')).insert(ignore_permissions=True) + + customer.mobile_no = "8989889890" + customer.save() + self.assertTrue(customer.customer_primary_contact) + frappe.delete_doc('Customer', customer.name) + def test_disabled_customer(self): make_test_records("Item") From af75d9a384d1ef53b8c89756cb2e1c8d450f2c55 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 28 Dec 2018 14:47:51 +0530 Subject: [PATCH 03/76] fix(website): Pagination in child item groups Item groups pages which had child item groups were not paged at all despite having next, prev buttons --- erpnext/setup/doctype/item_group/item_group.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 6bc9036195..f258f7d295 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -136,7 +136,10 @@ def get_child_groups_for_list_in_html(item_group, start, limit, search): rgt = ('<', item_group.rgt), ), or_filters = search_filters, - order_by = 'weightage desc, name asc') + order_by = 'weightage desc, name asc', + start = start, + limit = limit + ) return [get_item_for_list_in_html(r) for r in data] From fcbe410c2f1e606a30ebe7589ed8e03d7b22daf5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 28 Dec 2018 16:31:05 +0530 Subject: [PATCH 04/76] feat(stock-reco): Fetch items based on group warehouse --- .../stock_reconciliation.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 257434fb89..dacb53a587 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -270,24 +270,28 @@ class StockReconciliation(StockController): @frappe.whitelist() def get_items(warehouse, posting_date, posting_time): - items = frappe.get_list("Bin", fields=["item_code"], filters={"warehouse": warehouse}, as_list=1) + lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) + items = frappe.db.sql("""select item_code, warehouse from tabBin + where exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=`tabBin`.warehouse) + """, (lft, rgt)) - items += frappe.get_list("Item", fields=["name"], filters= {"is_stock_item": 1, "has_serial_no": 0, - "has_batch_no": 0, "has_variants": 0, "disabled": 0, "default_warehouse": warehouse}, - as_list=1) + items += frappe.db.sql("""select name, default_warehouse from tabItem + where exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=`tabItem`.default_warehouse) + and is_stock_item = 1 and has_serial_no = 0 and has_batch_no = 0 and has_variants = 0 and disabled = 0 + """, (lft, rgt)) res = [] - for item in set(items): - stock_bal = get_stock_balance(item[0], warehouse, posting_date, posting_time, + for item, wh in set(items): + stock_bal = get_stock_balance(item, wh, posting_date, posting_time, with_valuation_rate=True) - if frappe.db.get_value("Item",item[0],"disabled") == 0: + if frappe.db.get_value("Item", item, "disabled") == 0: res.append({ - "item_code": item[0], - "warehouse": warehouse, + "item_code": item, + "warehouse": wh, "qty": stock_bal[0], - "item_name": frappe.db.get_value('Item', item[0], 'item_name'), + "item_name": frappe.db.get_value('Item', item, 'item_name'), "valuation_rate": stock_bal[1], "current_qty": stock_bal[0], "current_valuation_rate": stock_bal[1] From 0c8411d085b9f0f6e3db645d24ae90503631d313 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 28 Dec 2018 16:53:00 +0530 Subject: [PATCH 05/76] test cases sucess --- erpnext/controllers/stock_controller.py | 20 +++++++++++-------- .../setup/doctype/item_group/item_group.py | 2 +- .../test_quality_inspection.py | 13 +++++++++--- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 63e89ab6e3..5ea3dee216 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -14,6 +14,7 @@ from erpnext.stock import get_warehouse_account_map class QualityInspectionRequiredError(frappe.ValidationError): pass class QualityInspectionRejectedError(frappe.ValidationError): pass +class QualityInspectionNotSubmittedError(frappe.ValidationError): pass class StockController(AccountsController): def validate(self): @@ -338,18 +339,21 @@ class StockController(AccountsController): qa_required = True elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse: qa_required = True + if self.docstatus == 1 and d.quality_inspection: + qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection) + if qa_doc.docstatus == 0: + link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) + frappe.msgprint(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx)) + raise QualityInspectionNotSubmittedError - if qa_required: + qa_failed = any([r.status=="Rejected" for r in qa_doc.readings]) + if qa_failed: + frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") + .format(d.idx, d.item_code), QualityInspectionRejectedError) + elif qa_required : frappe.msgprint(_("Quality Inspection required for Item {0}").format(d.item_code)) if self.docstatus==1: raise QualityInspectionRequiredError - elif self.docstatus == 1: - if d.quality_inspection: - qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection) - qa_failed = any([r.status=="Rejected" for r in qa_doc.readings]) - if qa_failed: - frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") - .format(d.idx, d.item_code), QualityInspectionRejectedError) def update_blanket_order(self): diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 6bc9036195..2c185019eb 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -147,7 +147,7 @@ def adjust_qty_for_expired_items(data): if item.get('has_batch_no') and item.get('website_warehouse'): stock_qty_dict = get_qty_in_stock( item.get('name'), 'website_warehouse', item.get('website_warehouse')) - qty = stock_qty_dict.stock_qty[0][0] + qty = stock_qty_dict.stock_qty[0][0] if stock_qty_dict.stock_qty else 0 item['in_stock'] = 1 if qty else 0 adjusted_data.append(item) diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 60cc9a0972..0f0b4016e2 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -6,7 +6,7 @@ import unittest from frappe.utils import nowdate from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note -from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError +from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError, QualityInspectionNotSubmittedError # test_records = frappe.get_test_records('Quality Inspection') @@ -19,7 +19,7 @@ class TestQualityInspection(unittest.TestCase): dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) self.assertRaises(QualityInspectionRequiredError, dn.submit) - qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected") + qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected", submit=True) dn.reload() self.assertRaises(QualityInspectionRejectedError, dn.submit) @@ -27,6 +27,12 @@ class TestQualityInspection(unittest.TestCase): dn.reload() dn.submit() + def test_qa_not_submit(self): + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, submit = False) + dn.items[0].quality_inspection = qa.name + self.assertRaises(QualityInspectionNotSubmittedError, dn.submit) + def create_quality_inspection(**args): args = frappe._dict(args) qa = frappe.new_doc("Quality Inspection") @@ -42,6 +48,7 @@ def create_quality_inspection(**args): "status": args.status }) qa.save() - qa.submit() + if args.submit: + qa.submit() return qa From 3637e14e9fa5bb0c8edf71ce65e0b22201befa1a Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 28 Dec 2018 17:11:52 +0530 Subject: [PATCH 06/76] refractor --- erpnext/projects/doctype/task/task.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 649d73a63f..53e3a5b68e 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -43,6 +43,10 @@ class Task(NestedSet): if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'")) + doc = frappe.get_doc("Project",self.project) + if self.exp_end_date and doc.expected_end_date and getdate(self.exp_end_date) > getdate(doc.expected_end_date) : + frappe.throw(_("Expected end date cannot be after Project: "+doc.name+" Expected end date")) + def validate_status(self): if self.status!=self.get_db_value("status") and self.status == "Closed": for d in self.depends_on: From a3b5f5f6e42c90e007b34240f7bc7f65198dc54e Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 1 Jan 2019 14:11:59 +0530 Subject: [PATCH 07/76] Test case --- erpnext/projects/doctype/task/task.py | 6 ++++-- erpnext/projects/doctype/task/test_task.py | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 53e3a5b68e..855e2006f9 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -12,6 +12,7 @@ from frappe.utils.nestedset import NestedSet class CircularReferenceError(frappe.ValidationError): pass +class EndDateConnotGreaterThanProjecteEndDateError(frappe.ValidationError): pass class Task(NestedSet): nsm_parent_field = 'parent_task' @@ -43,9 +44,10 @@ class Task(NestedSet): if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'")) - doc = frappe.get_doc("Project",self.project) + doc = frappe.get_doc("Project", self.project) if self.exp_end_date and doc.expected_end_date and getdate(self.exp_end_date) > getdate(doc.expected_end_date) : - frappe.throw(_("Expected end date cannot be after Project: "+doc.name+" Expected end date")) + frappe.msgprint(_("Expected end date cannot be after Project: '{0}' Expected end date").format(doc.name)) + raise EndDateConnotGreaterThanProjecteEndDateError def validate_status(self): if self.status!=self.get_db_value("status") and self.status == "Closed": diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 0966b76807..058f6b8d0d 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -5,7 +5,7 @@ import frappe import unittest from frappe.utils import getdate, nowdate, add_days -from erpnext.projects.doctype.task.task import CircularReferenceError +from erpnext.projects.doctype.task.task import CircularReferenceError, EndDateConnotGreaterThanProjecteEndDateError class TestTask(unittest.TestCase): def test_circular_reference(self): @@ -97,7 +97,16 @@ class TestTask(unittest.TestCase): self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue") -def create_task(subject, start=None, end=None, depends_on=None, project=None): + def test_end_date_validation(self): + task_end = create_task("Testing_Enddate_validation", add_days(nowdate(), -10), add_days(nowdate(), 5), save=False) + pro = frappe.get_doc("Project", task_end.project) + pro.expected_end_date = add_days(nowdate(), 20) + pro.save() + self.assertRaises(EndDateConnotGreaterThanProjecteEndDateError, task_end.save) + + + +def create_task(subject, start=None, end=None, save=True, depends_on=None, project=None): if not frappe.db.exists("Task", subject): task = frappe.new_doc('Task') task.status = "Open" @@ -105,7 +114,8 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None): task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() task.project = project or "_Test Project" - task.save() + if save: + task.save() else: task = frappe.get_doc("Task", subject) @@ -113,6 +123,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None): task.append("depends_on", { "task": depends_on }) - task.save() + if save: + task.save() return task \ No newline at end of file From 0885485a61d760f096bf25692722d6d4555de59f Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Tue, 1 Jan 2019 15:00:16 +0530 Subject: [PATCH 08/76] changes --- erpnext/controllers/stock_controller.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 5ea3dee216..0b19b7aaf9 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -343,8 +343,7 @@ class StockController(AccountsController): qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection) if qa_doc.docstatus == 0: link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) - frappe.msgprint(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx)) - raise QualityInspectionNotSubmittedError + frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError) qa_failed = any([r.status=="Rejected" for r in qa_doc.readings]) if qa_failed: @@ -432,4 +431,4 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1): gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) - return gl_entries \ No newline at end of file + return gl_entries From fac6b5962765515309e98b65c6baa08059632500 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Tue, 1 Jan 2019 16:33:22 +0500 Subject: [PATCH 09/76] Exclude opening invoices in Gross Profit Report --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2ed664c9bf..1bb3a8825f 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -316,7 +316,7 @@ class GrossProfitGenerator(object): on `tabSales Invoice Item`.parent = `tabSales Invoice`.name {sales_team_table} where - `tabSales Invoice`.docstatus=1 {conditions} {match_cond} + `tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening='No' {conditions} {match_cond} order by `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""" .format(conditions=conditions, sales_person_cols=sales_person_cols, From c349ae26b99dc8e97efc5e8cc8766f34dff25992 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 2 Jan 2019 09:31:11 +0530 Subject: [PATCH 10/76] Add patch to rename additional salary component - Should fix https://pastebin.com/xsNHPAh6 (happens when clicking links option in menu of Employee master) --- erpnext/patches.txt | 1 + ...me_additional_salary_component_additional_salary.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9b8a69d2b2..be8201a07d 100755 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -580,3 +580,4 @@ erpnext.patches.v11_0.update_delivery_trip_status erpnext.patches.v10_0.repost_gle_for_purchase_receipts_with_rejected_items erpnext.patches.v11_0.set_missing_gst_hsn_code erpnext.patches.v11_0.rename_bom_wo_fields +erpnext.patches.v11_0.rename_additional_salary_component_additional_salary \ No newline at end of file diff --git a/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py b/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py new file mode 100644 index 0000000000..8fa876dd74 --- /dev/null +++ b/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py @@ -0,0 +1,10 @@ +import frappe + +# this patch should have been included with this PR https://github.com/frappe/erpnext/pull/14302 + +def execute(): + if frappe.db.table_exists("Additional Salary Component"): + if not frappe.db.table_exists("Additional Salary"): + frappe.rename_doc("DocType", "Additional Salary Component", "Additional Salary") + + frappe.delete_doc('DocType', "Additional Salary Component") From 5865fcca3cf9096ee4099f0f6296a919593adc89 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 2 Jan 2019 14:36:07 +0530 Subject: [PATCH 11/76] fix: expense head of asset items in purchase invoice --- .../doctype/purchase_invoice/purchase_invoice.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index bfdf451f44..d28dc936bb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -206,6 +206,10 @@ class PurchaseInvoice(BuyingController): stock_not_billed_account = self.get_company_default("stock_received_but_not_billed") stock_items = self.get_stock_items() + asset_items = [d.is_fixed_asset for d in self.items if d.is_fixed_asset] + if len(asset_items) > 0: + asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") + if self.update_stock: self.validate_item_code() self.validate_warehouse() @@ -226,7 +230,8 @@ class PurchaseInvoice(BuyingController): item.expense_account = warehouse_account[item.warehouse]["account"] else: item.expense_account = stock_not_billed_account - + elif item.is_fixed_asset and d.pr_detail: + item.expense_account = asset_received_but_not_billed elif not item.expense_account and for_validate: throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) @@ -360,7 +365,10 @@ class PurchaseInvoice(BuyingController): def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) - self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") + if self.auto_accounting_for_stock: + self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") + else: + self.stock_received_but_not_billed = None self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") self.negative_expense_to_be_booked = 0.0 gl_entries = [] From 772e2c4e20330097dc271be1146e192f7520eff2 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 2 Jan 2019 15:59:58 +0530 Subject: [PATCH 12/76] test cases --- erpnext/assets/doctype/asset/test_asset.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d855873d5a..c5a7c3d0c3 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -9,6 +9,7 @@ from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, ad from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset from erpnext.assets.doctype.asset.asset import make_sales_invoice, make_purchase_invoice from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice class TestAsset(unittest.TestCase): def setUp(self): @@ -494,6 +495,15 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) + def test_expence_head(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=2, rate=200000.0, location="Test Location") + + doc = make_invoice(pr.name) + + self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) + + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): create_asset_category() From e194a655df8d856fccfb0a37b7e1b3969015f3c3 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Wed, 2 Jan 2019 16:09:34 +0500 Subject: [PATCH 13/76] fix: is_opening != 'Yes' --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- erpnext/setup/doctype/company/company_dashboard.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 1bb3a8825f..56bd7c2a87 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -316,7 +316,7 @@ class GrossProfitGenerator(object): on `tabSales Invoice Item`.parent = `tabSales Invoice`.name {sales_team_table} where - `tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening='No' {conditions} {match_cond} + `tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond} order by `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""" .format(conditions=conditions, sales_person_cols=sales_person_cols, diff --git a/erpnext/setup/doctype/company/company_dashboard.py b/erpnext/setup/doctype/company/company_dashboard.py index 61332267e7..5efcf3839f 100644 --- a/erpnext/setup/doctype/company/company_dashboard.py +++ b/erpnext/setup/doctype/company/company_dashboard.py @@ -13,7 +13,7 @@ def get_data(): 'goal_doctype_link': 'company', 'goal_field': 'base_grand_total', 'date_field': 'posting_date', - 'filter_str': 'docstatus = 1', + 'filter_str': "docstatus = 1 and is_opening != 'Yes'", 'aggregation': 'sum' }, From 016f29f09b5714f51ebb0f4e1c5d7590935df4e4 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Wed, 2 Jan 2019 16:22:22 +0500 Subject: [PATCH 14/76] fix: error due to passing None in scrub --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 56bd7c2a87..7ecc695520 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -200,7 +200,7 @@ class GrossProfitGenerator(object): def skip_row(self, row, product_bundles): if self.filters.get("group_by") != "Invoice": - if not row.get(scrub(self.filters.get("group_by"))): + if not row.get(scrub(self.filters.get("group_by", ""))): return True elif row.get("is_return") == 1: return True From 4c8c50e464680ceb4e5742120b05d232fe60deac Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 2 Jan 2019 17:52:28 +0530 Subject: [PATCH 15/76] test case fixed --- erpnext/projects/doctype/task/task.py | 9 +++++---- erpnext/projects/doctype/task/test_task.py | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 855e2006f9..63db6cdea5 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -44,10 +44,11 @@ class Task(NestedSet): if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'")) - doc = frappe.get_doc("Project", self.project) - if self.exp_end_date and doc.expected_end_date and getdate(self.exp_end_date) > getdate(doc.expected_end_date) : - frappe.msgprint(_("Expected end date cannot be after Project: '{0}' Expected end date").format(doc.name)) - raise EndDateConnotGreaterThanProjecteEndDateError + if(self.project): + doc = frappe.get_doc("Project", self.project) + if self.exp_end_date and doc.expected_end_date and getdate(self.exp_end_date) > getdate(doc.expected_end_date) : + frappe.msgprint(_("Expected end date cannot be after Project: '{0}' Expected end date").format(doc.name)) + raise EndDateConnotGreaterThanProjecteEndDateError def validate_status(self): if self.status!=self.get_db_value("status") and self.status == "Closed": diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 058f6b8d0d..1585e798be 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -9,7 +9,7 @@ from erpnext.projects.doctype.task.task import CircularReferenceError, EndDateCo class TestTask(unittest.TestCase): def test_circular_reference(self): - task1 = create_task("_Test Task 1", nowdate(), add_days(nowdate(), 10)) + task1 = create_task("_Test Task 1", add_days(nowdate(), -15), add_days(nowdate(), -10)) task2 = create_task("_Test Task 2", add_days(nowdate(), 11), add_days(nowdate(), 15), task1.name) task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name) @@ -98,15 +98,15 @@ class TestTask(unittest.TestCase): self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue") def test_end_date_validation(self): - task_end = create_task("Testing_Enddate_validation", add_days(nowdate(), -10), add_days(nowdate(), 5), save=False) + task_end = create_task("Testing_Enddate_validation", add_days(nowdate(), 35), add_days(nowdate(), 45), save=False) pro = frappe.get_doc("Project", task_end.project) - pro.expected_end_date = add_days(nowdate(), 20) + pro.expected_end_date = add_days(nowdate(), 40) pro.save() self.assertRaises(EndDateConnotGreaterThanProjecteEndDateError, task_end.save) -def create_task(subject, start=None, end=None, save=True, depends_on=None, project=None): +def create_task(subject, start=None, end=None, depends_on=None, project=None, save=True): if not frappe.db.exists("Task", subject): task = frappe.new_doc('Task') task.status = "Open" From 1ff1fc47251012753b4b72248e83e29a3bef9435 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 2 Jan 2019 17:56:08 +0530 Subject: [PATCH 16/76] [Fix] Negative amount showing in the bank clearance summary --- .../bank_clearance_summary.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index 1ec0abc3bf..13424dbcb5 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -35,16 +35,22 @@ def get_conditions(filters): def get_entries(filters): conditions = get_conditions(filters) - journal_entries = frappe.db.sql("""select "Journal Entry", jv.name, jv.posting_date, - jv.cheque_no, jv.clearance_date, jvd.against_account, (jvd.debit - jvd.credit) - from `tabJournal Entry Account` jvd, `tabJournal Entry` jv - where jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {0} - order by posting_date DESC, jv.name DESC""".format(conditions), filters, as_list=1) + journal_entries = frappe.db.sql("""SELECT + "Journal Entry", jv.name, jv.posting_date, jv.cheque_no, jv.clearance_date, jvd.against_account, + if((jvd.debit - jvd.credit) < 0, (jvd.debit - jvd.credit) * -1, (jvd.debit - jvd.credit)) + FROM + `tabJournal Entry Account` jvd, `tabJournal Entry` jv + WHERE + jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {0} + order by posting_date DESC, jv.name DESC""".format(conditions), filters, as_list=1) - payment_entries = frappe.db.sql("""select "Payment Entry", name, posting_date, - reference_no, clearance_date, party, if(paid_from=%(account)s, paid_amount, received_amount) - from `tabPayment Entry` - where docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {0} - order by posting_date DESC, name DESC""".format(conditions), filters, as_list=1) + payment_entries = frappe.db.sql("""SELECT + "Payment Entry", name, posting_date, reference_no, clearance_date, party, + if(paid_from=%(account)s, paid_amount, received_amount) + FROM + `tabPayment Entry` + WHERE + docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {0} + order by posting_date DESC, name DESC""".format(conditions), filters, as_list=1) return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate())) \ No newline at end of file From 53c040f83874a8c50e82e942197995829bed5815 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Thu, 3 Jan 2019 12:50:18 +0530 Subject: [PATCH 17/76] Gst number validation fix --- 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 b878a1ea5d..3f293ce14f 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -11,7 +11,7 @@ def validate_gstin_for_india(doc, method): if doc.gstin: doc.gstin = doc.gstin.upper() if doc.gstin not in ["NA", "na"]: - p = re.compile("[0-9]{2}[a-zA-Z]{5}[0-9]{4}[a-zA-Z]{1}[1-9A-Za-z]{1}[Z]{1}[0-9a-zA-Z]{1}") + p = re.compile("[0-9]{2}[0-9A-Za-z]{10}[0-9A-Za-z]{1}[0-9a-zA-Z]{1}[0-9a-zA-Z]{1}") if not p.match(doc.gstin): frappe.throw(_("Invalid GSTIN or Enter NA for Unregistered")) From 47c9826b6f12d87ec5ac254bbc00e3f45222d385 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 3 Jan 2019 15:24:59 +0530 Subject: [PATCH 18/76] test(reco-warehouse): Get items for group warehouse --- .../stock_reconciliation/stock_reconciliation.py | 1 - .../test_stock_reconciliation.py | 15 ++++++++++++++- erpnext/stock/doctype/warehouse/test_warehouse.py | 6 ++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index dacb53a587..561868f8a3 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -286,7 +286,6 @@ def get_items(warehouse, posting_date, posting_time): with_valuation_rate=True) if frappe.db.get_value("Item", item, "disabled") == 0: - res.append({ "item_code": item, "warehouse": wh, diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 212bb51185..78ff915011 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -10,7 +10,9 @@ from frappe.utils import flt, nowdate, nowtime from erpnext.accounts.utils import get_stock_and_account_difference from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after -from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError +from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.doctype.item.test_item import make_item class TestStockReconciliation(unittest.TestCase): def setUp(self): @@ -79,6 +81,17 @@ class TestStockReconciliation(unittest.TestCase): set_perpetual_inventory(0) + def test_get_items(self): + create_warehouse("_Test Warehouse Group 1", {"is_group": 1}) + create_warehouse("_Test Warehouse Ledger 1", {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"}) + item1 = make_item("_Test Stock Reco Item", {"default_warehouse": "_Test Warehouse Ledger 1 - _TC", + "is_stock_item": 1, "opening_stock": 100, "valuation_rate": 100}) + + items = get_items("_Test Warehouse Group 1 - _TC", nowdate(), nowtime()) + + self.assertEqual(["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100], + [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]]) + def insert_existing_sle(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index d010313534..961d0a7614 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -90,7 +90,7 @@ class TestWarehouse(unittest.TestCase): self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": "Test Warehouse for Merging 2 - _TC"})) -def create_warehouse(warehouse_name): +def create_warehouse(warehouse_name, properties=None): if not frappe.db.exists("Warehouse", warehouse_name + " - _TC"): w = frappe.new_doc("Warehouse") w.warehouse_name = warehouse_name @@ -98,11 +98,13 @@ def create_warehouse(warehouse_name): w.company = "_Test Company" make_account_for_warehouse(warehouse_name, w) w.account = warehouse_name + " - _TC" + if properties: + w.update(properties) w.save() def make_account_for_warehouse(warehouse_name, warehouse_obj): if not frappe.db.exists("Account", warehouse_name + " - _TC"): - parent_account = frappe.db.get_value('Account', + parent_account = frappe.db.get_value('Account', {'company': warehouse_obj.company, 'is_group':1, 'account_type': 'Stock'},'name') account = create_account(account_name=warehouse_name, \ account_type="Stock", parent_account= parent_account, company=warehouse_obj.company) \ No newline at end of file From 547229fef12221fc4e1f9c86eafb0397d361feb3 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Thu, 3 Jan 2019 23:20:38 +0530 Subject: [PATCH 19/76] PAN card validation in GST --- 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 3f293ce14f..aad998b9c1 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -11,7 +11,7 @@ def validate_gstin_for_india(doc, method): if doc.gstin: doc.gstin = doc.gstin.upper() if doc.gstin not in ["NA", "na"]: - p = re.compile("[0-9]{2}[0-9A-Za-z]{10}[0-9A-Za-z]{1}[0-9a-zA-Z]{1}[0-9a-zA-Z]{1}") + p = re.compile("[0-9]{2}[a-zA-Z]{4}[0-9a-zA-Z]{1}[0-9]{4}[a-zA-Z]{1}[1-9a-zA-Z]{1}[0-9a-zA-Z]{1}[0-9a-zA-Z]{1}") if not p.match(doc.gstin): frappe.throw(_("Invalid GSTIN or Enter NA for Unregistered")) From f2b29a0488af667965a3bde1b21c3ced3994161b Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 4 Jan 2019 10:54:05 +0530 Subject: [PATCH 20/76] typo --- erpnext/projects/doctype/task/task.py | 11 ++++++----- erpnext/projects/doctype/task/test_task.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 63db6cdea5..a470ed9aa4 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -12,7 +12,7 @@ from frappe.utils.nestedset import NestedSet class CircularReferenceError(frappe.ValidationError): pass -class EndDateConnotGreaterThanProjecteEndDateError(frappe.ValidationError): pass +class EndDateCannotGreaterThanProjecteEndDateError(frappe.ValidationError): pass class Task(NestedSet): nsm_parent_field = 'parent_task' @@ -45,10 +45,11 @@ class Task(NestedSet): frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'")) if(self.project): - doc = frappe.get_doc("Project", self.project) - if self.exp_end_date and doc.expected_end_date and getdate(self.exp_end_date) > getdate(doc.expected_end_date) : - frappe.msgprint(_("Expected end date cannot be after Project: '{0}' Expected end date").format(doc.name)) - raise EndDateConnotGreaterThanProjecteEndDateError + if frappe.db.exists("Project", self.project): + doc = frappe.get_doc("Project", self.project) + if self.exp_end_date and doc.expected_end_date and getdate(self.exp_end_date) > getdate(doc.expected_end_date) : + frappe.msgprint(_("Expected end date cannot be after Project: '{0}' Expected end date").format(doc.name)) + raise EndDateCannotGreaterThanProjecteEndDateError def validate_status(self): if self.status!=self.get_db_value("status") and self.status == "Closed": diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 1585e798be..b9d7958751 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -5,7 +5,7 @@ import frappe import unittest from frappe.utils import getdate, nowdate, add_days -from erpnext.projects.doctype.task.task import CircularReferenceError, EndDateConnotGreaterThanProjecteEndDateError +from erpnext.projects.doctype.task.task import CircularReferenceError, EndDateCannotGreaterThanProjecteEndDateError class TestTask(unittest.TestCase): def test_circular_reference(self): @@ -102,7 +102,7 @@ class TestTask(unittest.TestCase): pro = frappe.get_doc("Project", task_end.project) pro.expected_end_date = add_days(nowdate(), 40) pro.save() - self.assertRaises(EndDateConnotGreaterThanProjecteEndDateError, task_end.save) + self.assertRaises(EndDateCannotGreaterThanProjecteEndDateError, task_end.save) From 68aedfd1764a0925ae8761fe8b782f2531d1e936 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 4 Jan 2019 11:00:07 +0530 Subject: [PATCH 21/76] typo --- erpnext/projects/doctype/task/task.py | 5 ++--- erpnext/projects/doctype/task/test_task.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index a470ed9aa4..08abba31f4 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -12,7 +12,7 @@ from frappe.utils.nestedset import NestedSet class CircularReferenceError(frappe.ValidationError): pass -class EndDateCannotGreaterThanProjecteEndDateError(frappe.ValidationError): pass +class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass class Task(NestedSet): nsm_parent_field = 'parent_task' @@ -48,8 +48,7 @@ class Task(NestedSet): if frappe.db.exists("Project", self.project): doc = frappe.get_doc("Project", self.project) if self.exp_end_date and doc.expected_end_date and getdate(self.exp_end_date) > getdate(doc.expected_end_date) : - frappe.msgprint(_("Expected end date cannot be after Project: '{0}' Expected end date").format(doc.name)) - raise EndDateCannotGreaterThanProjecteEndDateError + frappe.throw(_("Expected end date cannot be after Project: '{0}' Expected end date").format(doc.name), EndDateCannotBeGreaterThanProjectEndDateError) def validate_status(self): if self.status!=self.get_db_value("status") and self.status == "Closed": diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index b9d7958751..6fb5412473 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -5,7 +5,7 @@ import frappe import unittest from frappe.utils import getdate, nowdate, add_days -from erpnext.projects.doctype.task.task import CircularReferenceError, EndDateCannotGreaterThanProjecteEndDateError +from erpnext.projects.doctype.task.task import CircularReferenceError, EndDateCannotBeGreaterThanProjectEndDateError class TestTask(unittest.TestCase): def test_circular_reference(self): @@ -102,7 +102,7 @@ class TestTask(unittest.TestCase): pro = frappe.get_doc("Project", task_end.project) pro.expected_end_date = add_days(nowdate(), 40) pro.save() - self.assertRaises(EndDateCannotGreaterThanProjecteEndDateError, task_end.save) + self.assertRaises(EndDateCannotBeGreaterThanProjectEndDateError, task_end.save) From add6bf35a323b978ccfa45cf980e937117f75734 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 4 Jan 2019 11:36:30 +0530 Subject: [PATCH 22/76] Fix: Employee Onboarding/Seperation task Assignment --- erpnext/hr/utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index f35eb5919e..5058006431 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -54,10 +54,17 @@ class EmployeeBoardingController(Document): where parenttype='User' and role=%s''', activity.role) users = users + user_list + if "Administrator" in users: + users.remove("Administrator") + + # assign the task the users if users: + print(users) self.assign_task_to_users(task, set(users)) + users = [] + def assign_task_to_users(self, task, users): for user in users: args = { From bb6a7eb9d3b9e901c407b62ced4e1ff43973c43d Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 4 Jan 2019 12:32:51 +0530 Subject: [PATCH 23/76] refractor --- erpnext/hr/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 5058006431..eb55037566 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -60,7 +60,6 @@ class EmployeeBoardingController(Document): # assign the task the users if users: - print(users) self.assign_task_to_users(task, set(users)) users = [] From 75ab0426326d2b6ba8c98115f6ce168551d39b7c Mon Sep 17 00:00:00 2001 From: Himanshu Date: Fri, 4 Jan 2019 17:13:43 +0530 Subject: [PATCH 24/76] removed lowercase regex --- 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 aad998b9c1..52956052aa 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -11,7 +11,7 @@ def validate_gstin_for_india(doc, method): if doc.gstin: doc.gstin = doc.gstin.upper() if doc.gstin not in ["NA", "na"]: - p = re.compile("[0-9]{2}[a-zA-Z]{4}[0-9a-zA-Z]{1}[0-9]{4}[a-zA-Z]{1}[1-9a-zA-Z]{1}[0-9a-zA-Z]{1}[0-9a-zA-Z]{1}") + p = re.compile("[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}[0-9A-Z]{1}") if not p.match(doc.gstin): frappe.throw(_("Invalid GSTIN or Enter NA for Unregistered")) From 8e47a543f640c15a3f791cbbf2b27b9e348e9972 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Jan 2019 12:07:17 +0530 Subject: [PATCH 25/76] (fix) multiple years in yearly view --- erpnext/selling/report/sales_analytics/sales_analytics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index c078a08249..46cb5ff022 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -235,7 +235,7 @@ class Analytics(object): }.get(self.filters.range, 1) self.periodic_daterange = [] - for dummy in range(1, 53, increment): + for dummy in range(1, 53): if self.filters.range == "Weekly": period_end_date = from_date + relativedelta(days=6) else: From 676a106d7a73ca3a638dbc900206bdeebcff276c Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Jan 2019 13:17:31 +0530 Subject: [PATCH 26/76] (refactor) removed unnecessary code --- erpnext/selling/report/sales_analytics/sales_analytics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 46cb5ff022..8ecf3254d0 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -40,7 +40,7 @@ class Analytics(object): "fieldtype": "Data", "width": 140 }) - for dummy, end_date in self.periodic_daterange: + for end_date in self.periodic_daterange: period = self.get_period(end_date) self.columns.append({ "label": _(period), @@ -169,7 +169,7 @@ class Analytics(object): "entity_name": self.entity_names.get(entity) } total = 0 - for dummy, end_date in self.periodic_daterange: + for end_date in self.periodic_daterange: period = self.get_period(end_date) amount = flt(period_data.get(period, 0.0)) row[scrub(period)] = amount @@ -188,7 +188,7 @@ class Analytics(object): "indent": self.depth_map.get(d.name) } total = 0 - for dummy, end_date in self.periodic_daterange: + for end_date in self.periodic_daterange: period = self.get_period(end_date) amount = flt(self.entity_periodic_data.get(d.name, {}).get(period, 0.0)) row[scrub(period)] = amount @@ -243,8 +243,8 @@ class Analytics(object): if period_end_date > to_date: period_end_date = to_date - self.periodic_daterange.append([from_date, period_end_date]) + self.periodic_daterange.append(period_end_date) from_date = period_end_date + relativedelta(days=1) if period_end_date == to_date: break From e2ea987610e8f31cf9d955d9d08f1c4fa7455ddf Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 4 Jan 2019 23:34:54 +0530 Subject: [PATCH 27/76] fixed edge cases for yearly, weekly and monthly views --- .../report/sales_analytics/sales_analytics.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 8ecf3254d0..3239fc626f 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _, scrub -from frappe.utils import getdate, flt +from frappe.utils import getdate, flt, add_to_date, add_days from six import iteritems from erpnext.accounts.utils import get_fiscal_year @@ -219,12 +219,11 @@ class Analytics(object): period = "Quarter " + str(((posting_date.month-1)//3)+1) +" " + str(posting_date.year) else: year = get_fiscal_year(posting_date, company=self.filters.company) - period = str(year[2]) - + period = str(year[0]) return period def get_period_date_ranges(self): - from dateutil.relativedelta import relativedelta + from dateutil.relativedelta import relativedelta, MO from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) increment = { @@ -234,18 +233,26 @@ class Analytics(object): "Yearly": 12 }.get(self.filters.range, 1) + if self.filters.range in ['Monthly', 'Quarterly']: + from_date = from_date.replace(day = 1) + elif self.filters.range == "Yearly": + from_date = get_fiscal_year(from_date)[1] + else: + from_date = from_date + relativedelta(from_date, weekday=MO(-1)) + self.periodic_daterange = [] for dummy in range(1, 53): if self.filters.range == "Weekly": - period_end_date = from_date + relativedelta(days=6) + period_end_date = add_days(from_date, 6) else: - period_end_date = from_date + relativedelta(months=increment, days=-1) + period_end_date = add_to_date(from_date, months=increment, days=-1) if period_end_date > to_date: period_end_date = to_date self.periodic_daterange.append(period_end_date) - from_date = period_end_date + relativedelta(days=1) + + from_date = add_days(period_end_date, 1) if period_end_date == to_date: break From 5efc7973ea5177e95e05084b510d42dd0549532d Mon Sep 17 00:00:00 2001 From: finbyz Date: Sat, 5 Jan 2019 11:12:11 +0530 Subject: [PATCH 28/76] Minor Fix for Rounded Total --- erpnext/controllers/accounts_controller.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 86ceb2e4ab..140694893c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -604,13 +604,14 @@ class AccountsController(TransactionBase): advance.account_currency) if advance.account_currency == self.currency: - order_total = self.grand_total - formatted_order_total = fmt_money(order_total, precision=self.precision("grand_total"), - currency=advance.account_currency) + order_total = self.get("rounded_total") or self.grand_total + precision = "rounded_total" if self.get("rounded_total") else "grand_total" else: - order_total = self.base_grand_total - formatted_order_total = fmt_money(order_total, precision=self.precision("base_grand_total"), - currency=advance.account_currency) + order_total = self.get("base_rounded_total") or self.base_grand_total + precision = "base_rounded_total" if self.get("base_rounded_total") else "base_grand_total" + + formatted_order_total = fmt_money(order_total, precision=self.precision(precision), + currency=advance.account_currency) if self.currency == self.company_currency and advance_paid > order_total: frappe.throw(_("Total advance ({0}) against Order {1} cannot be greater than the Grand Total ({2})") From 23a1b9895791edb3956e42274521417c6805fd3a Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 7 Jan 2019 13:10:18 +0530 Subject: [PATCH 29/76] fix: remove unnecessary code --- erpnext/hr/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index eb55037566..2dc0ab0216 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -62,8 +62,6 @@ class EmployeeBoardingController(Document): if users: self.assign_task_to_users(task, set(users)) - users = [] - def assign_task_to_users(self, task, users): for user in users: args = { From 3c74266763f2c7b1e85404a306f81fd3a798f746 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 7 Jan 2019 13:13:16 +0530 Subject: [PATCH 30/76] fix: PEP8 recommended whitspace --- erpnext/hr/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 2dc0ab0216..02262012f1 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -57,7 +57,6 @@ class EmployeeBoardingController(Document): if "Administrator" in users: users.remove("Administrator") - # assign the task the users if users: self.assign_task_to_users(task, set(users)) From d40743a570948f5d1cd113755bdede4cb51e8d1a Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 7 Jan 2019 13:38:43 +0530 Subject: [PATCH 31/76] fix: 14th digit may not be zero --- 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 52956052aa..9b7edc5338 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -11,7 +11,7 @@ def validate_gstin_for_india(doc, method): if doc.gstin: doc.gstin = doc.gstin.upper() if doc.gstin not in ["NA", "na"]: - p = re.compile("[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}[0-9A-Z]{1}") + p = re.compile("[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}") if not p.match(doc.gstin): frappe.throw(_("Invalid GSTIN or Enter NA for Unregistered")) From f733a390898e884bd08605aade8575d6a3047e11 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 7 Jan 2019 14:24:01 +0530 Subject: [PATCH 32/76] fix: remove trailing whitespace --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 140694893c..83ba438ad6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -609,7 +609,7 @@ class AccountsController(TransactionBase): else: order_total = self.get("base_rounded_total") or self.base_grand_total precision = "base_rounded_total" if self.get("base_rounded_total") else "base_grand_total" - + formatted_order_total = fmt_money(order_total, precision=self.precision(precision), currency=advance.account_currency) From 0f3ccba741ca1f0dfbd73f542402df83c8740f9c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 8 Jan 2019 20:23:01 +0530 Subject: [PATCH 33/76] Update test_asset.py --- erpnext/assets/doctype/asset/test_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index c5a7c3d0c3..65629d2818 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -495,7 +495,7 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) - def test_expence_head(self): + def test_expense_head(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location") From 1516e296095a90d61a227fde20d97f50c2f0fa80 Mon Sep 17 00:00:00 2001 From: hiousi Date: Tue, 8 Jan 2019 18:34:08 +0100 Subject: [PATCH 34/76] [fix] variant can not get its price get_price_list_rate() return empty dict before trying to get the item price for a variant. --- erpnext/stock/get_item_details.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 692fe5d0fb..7be4f9f8fb 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -369,6 +369,10 @@ def get_price_list_rate(args, item_doc, out): validate_conversion_rate(args, meta) price_list_rate = get_price_list_rate_for(args, item_doc.name) or 0 + + # variant + if not price_list_rate and item_doc.variant_of: + price_list_rate = get_price_list_rate_for(args, item_doc.variant_of) # insert in database if not price_list_rate: @@ -376,10 +380,6 @@ def get_price_list_rate(args, item_doc, out): insert_item_price(args) return {} - # variant - if not price_list_rate and item_doc.variant_of: - price_list_rate = get_price_list_rate_for(args, item_doc.variant_of) - out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \ / flt(args.conversion_rate) From 96714384354bd5d4bec5a68173d0e6950116f418 Mon Sep 17 00:00:00 2001 From: hiousi Date: Tue, 8 Jan 2019 22:23:45 +0100 Subject: [PATCH 35/76] Update get_item_details.py --- erpnext/stock/get_item_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 7be4f9f8fb..3f85521207 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -369,7 +369,7 @@ def get_price_list_rate(args, item_doc, out): validate_conversion_rate(args, meta) price_list_rate = get_price_list_rate_for(args, item_doc.name) or 0 - + # variant if not price_list_rate and item_doc.variant_of: price_list_rate = get_price_list_rate_for(args, item_doc.variant_of) From 2689dea72a15a0bd2949a4e481a23f5bfb213360 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Wed, 9 Jan 2019 13:43:06 +0500 Subject: [PATCH 36/76] fix: Give higher precedence to set_warehouse in get_item_details while setting item level warehouse --- erpnext/public/js/controllers/transaction.js | 1 + erpnext/stock/get_item_details.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 035c58d6d2..9dd9bebb4d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -416,6 +416,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ item_code: item.item_code, barcode: item.barcode, serial_no: item.serial_no, + set_warehouse: me.frm.doc.set_warehouse, warehouse: item.warehouse, customer: me.frm.doc.customer, supplier: me.frm.doc.supplier, diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 692fe5d0fb..bafe545d6f 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -36,6 +36,7 @@ def get_item_details(args): "is_subcontracted": "Yes" / "No", "ignore_pricing_rule": 0/1 "project": "" + "set_warehouse": "" } """ args = process_args(args) @@ -189,7 +190,6 @@ def get_basic_details(args, item): "project": "", barcode: "", serial_no: "", - warehouse: "", currency: "", update_stock: "", price_list: "", @@ -219,7 +219,7 @@ def get_basic_details(args, item): item_defaults = get_item_defaults(item.name, args.company) item_group_defaults = get_item_group_defaults(item.name, args.company) - warehouse = user_default_warehouse or item_defaults.get("default_warehouse") or\ + warehouse = args.get("set_warehouse") or user_default_warehouse or item_defaults.get("default_warehouse") or\ item_group_defaults.get("default_warehouse") or args.warehouse if args.get('doctype') == "Material Request" and not args.get('material_request_type'): From d29ee9730765473afddeebc10c81e302d90dd0c8 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Wed, 9 Jan 2019 17:09:11 +0530 Subject: [PATCH 37/76] Addition of GST fields in Sales Order,Purchase Order,Purchase Receipt --- erpnext/patches.txt | 2 +- erpnext/regional/india/setup.py | 15 +- .../doctype/sales_order/sales_order.js | 14 ++ .../doctype/sales_order/sales_order.json | 128 +++++++++++++----- 4 files changed, 124 insertions(+), 35 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9b8a69d2b2..43209e3e50 100755 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -572,7 +572,7 @@ execute:frappe.delete_doc_if_exists("Page", "sales-analytics") execute:frappe.delete_doc_if_exists("Page", "purchase-analytics") execute:frappe.delete_doc_if_exists("Page", "stock-analytics") execute:frappe.delete_doc_if_exists("Page", "production-analytics") -erpnext.patches.v11_0.ewaybill_fields_gst_india #2018-11-13 +erpnext.patches.v11_0.ewaybill_fields_gst_india #2018-11-13 #2019-01-09 erpnext.patches.v11_0.drop_column_max_days_allowed erpnext.patches.v11_0.change_healthcare_desktop_icons erpnext.patches.v10_0.update_user_image_in_employee diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 7813cc0fd6..ec4da0db65 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -126,6 +126,9 @@ def make_custom_fields(update=True): dict(fieldname='place_of_supply', label='Place of Supply', fieldtype='Data', insert_after='shipping_address', print_hide=1, read_only=0), + ] + + purchase_invoice_itc_fields = [ dict(fieldname='eligibility_for_itc', label='Eligibility For ITC', fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1, options='input\ninput service\ncapital goods\nineligible', default="ineligible"), @@ -152,6 +155,9 @@ def make_custom_fields(update=True): dict(fieldname='company_gstin', label='Company GSTIN', fieldtype='Data', insert_after='company_address', fetch_from='company_address.gstin', print_hide=1), + ] + + sales_invoice_shipping_fields = [ dict(fieldname='port_code', label='Port Code', fieldtype='Data', insert_after='reason_for_issuing_document', print_hide=1, depends_on="eval:doc.invoice_type=='Export' "), @@ -214,9 +220,12 @@ def make_custom_fields(update=True): dict(fieldname='gst_state_number', label='GST State Number', fieldtype='Data', insert_after='gst_state', read_only=1), ], - 'Purchase Invoice': invoice_gst_fields + purchase_invoice_gst_fields, - 'Sales Invoice': invoice_gst_fields + sales_invoice_gst_fields, - 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields, + 'Purchase Invoice': invoice_gst_fields + purchase_invoice_gst_fields + purchase_invoice_itc_fields, + 'Purchase Order': purchase_invoice_gst_fields, + 'Purchase Receipt': purchase_invoice_gst_fields, + 'Sales Invoice': invoice_gst_fields + sales_invoice_gst_fields + sales_invoice_shipping_fields, + 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, + 'Sales Order': sales_invoice_gst_fields, 'Sales Taxes and Charges Template': inter_state_gst_field, 'Purchase Taxes and Charges Template': inter_state_gst_field, 'Item': [ diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 54d7654c2d..96d2fc8f83 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -17,6 +17,20 @@ frappe.ui.form.on("Sales Order", { // formatter for material request item frm.set_indicator_formatter('item_code', function(doc) { return (doc.stock_qty<=doc.delivered_qty) ? "green" : "orange" }) + + frm.set_query('company_address', function(doc) { + if(!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + query: 'frappe.contacts.doctype.address.address.address_query', + filters: { + link_doctype: 'Company', + link_name: doc.company + } + }; + }) }, refresh: function(frm) { if(frm.doc.docstatus == 1 && frm.doc.status == 'To Deliver and Bill') { diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 0f3b6776ca..b916505a0f 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -747,6 +747,71 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company_address_display", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company_address", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Company Address", + "length": 0, + "no_copy": 0, + "options": "Address", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1128,6 +1193,7 @@ "label": "Price List Exchange Rate", "length": 0, "no_copy": 0, + "options": "", "permlevel": 0, "precision": "9", "print_hide": 1, @@ -1270,37 +1336,37 @@ "unique": 0 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "scan_barcode", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Scan Barcode", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scan_barcode", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scan Barcode", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { "allow_bulk_edit": 1, "allow_in_quick_entry": 0, @@ -4015,7 +4081,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-11-12 20:00:35.272747", + "modified": "2019-01-09 16:51:47.917329", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", From 2825b929c1bb0c84c067e1dcff0a31feffbf7146 Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Wed, 9 Jan 2019 19:15:10 +0530 Subject: [PATCH 38/76] fix(GSTIN Validation - india): added checksum validation for GSTIN --- erpnext/regional/india/utils.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 9b7edc5338..c4bfe915dc 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -12,8 +12,8 @@ def validate_gstin_for_india(doc, method): doc.gstin = doc.gstin.upper() if doc.gstin not in ["NA", "na"]: p = re.compile("[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}") - if not p.match(doc.gstin): - frappe.throw(_("Invalid GSTIN or Enter NA for Unregistered")) + if not p.match(doc.gstin) or doc.gstin != get_gstin_with_check_digit(doc.gstin[:-1]): + frappe.throw(_("Invalid GSTIN!! Check for typos or Enter NA for Unregistered")) if not doc.gst_state: if doc.state in states: @@ -25,6 +25,28 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("First 2 digits of GSTIN should match with State number {0}") .format(doc.gst_state_number)) +def get_gstin_with_check_digit(gstin_without_check_digit): + ''' Function to get the check digit for the gstin. + + param: gstin_without_check_digit + return: GSTIN with check digit + ''' + factor = 1 + total = 0 + code_point_chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + input_chars = gstin_without_check_digit.strip() + if not input_chars: + frappe.throw(_("GSTIN supplied for checkdigit calculation is blank")) + mod = len(code_point_chars) + for char in input_chars: + digit = factor * code_point_chars.find(char) + if digit < 0: + frappe.throw(_("GSTIN supplied for checkdigit contains invalid character")) + digit = (digit / mod) + (digit % mod) + total += digit + factor = 2 if factor == 1 else 1 + return ''.join([gstin_without_check_digit,code_point_chars[((mod - (total % mod)) % mod)]]) + def get_itemised_tax_breakup_header(item_doctype, tax_accounts): if frappe.get_meta(item_doctype).has_field('gst_hsn_code'): return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts From 07cf4e8f5b03f0a7229ac0120162b73a124bd8bb Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 10 Jan 2019 11:07:51 +0530 Subject: [PATCH 39/76] fix: use division consistent with Python 3 & other changes --- erpnext/regional/india/utils.py | 55 +++++++++++++++++---------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index c4bfe915dc..9f161afd57 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -8,44 +8,47 @@ def validate_gstin_for_india(doc, method): if not hasattr(doc, 'gstin'): return - if doc.gstin: - doc.gstin = doc.gstin.upper() - if doc.gstin not in ["NA", "na"]: - p = re.compile("[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}") - if not p.match(doc.gstin) or doc.gstin != get_gstin_with_check_digit(doc.gstin[:-1]): - frappe.throw(_("Invalid GSTIN!! Check for typos or Enter NA for Unregistered")) + doc.gstin = doc.gstin.upper().strip() + if not doc.gstin or doc.gstin == 'NA': + return - if not doc.gst_state: - if doc.state in states: - doc.gst_state = doc.state + if len(doc.gstin) != 15: + frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters.")) - if doc.gst_state: - doc.gst_state_number = state_numbers[doc.gst_state] - if doc.gstin and doc.gstin != "NA" and doc.gst_state_number != doc.gstin[:2]: - frappe.throw(_("First 2 digits of GSTIN should match with State number {0}") - .format(doc.gst_state_number)) + p = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$") + if not p.match(doc.gstin): + frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN.")) -def get_gstin_with_check_digit(gstin_without_check_digit): - ''' Function to get the check digit for the gstin. + validate_gstin_check_digit(doc.gstin) - param: gstin_without_check_digit - return: GSTIN with check digit - ''' + if not doc.gst_state and doc.state: + state = doc.state.lower() + states_lowercase = {s.lower():s for s in states} + if state in states_lowercase: + doc.gst_state = states_lowercase[state] + else: + return + + doc.gst_state_number = state_numbers[doc.gst_state] + if doc.gst_state_number != doc.gstin[:2]: + frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") + .format(doc.gst_state_number)) + +def validate_gstin_check_digit(gstin): + ''' Function to validate the check digit of the GSTIN.''' factor = 1 total = 0 code_point_chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' - input_chars = gstin_without_check_digit.strip() - if not input_chars: - frappe.throw(_("GSTIN supplied for checkdigit calculation is blank")) mod = len(code_point_chars) + input_chars = gstin[:-1] for char in input_chars: digit = factor * code_point_chars.find(char) - if digit < 0: - frappe.throw(_("GSTIN supplied for checkdigit contains invalid character")) - digit = (digit / mod) + (digit % mod) + digit = (digit // mod) + (digit % mod) total += digit factor = 2 if factor == 1 else 1 - return ''.join([gstin_without_check_digit,code_point_chars[((mod - (total % mod)) % mod)]]) + if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: + frappe.throw(_("Invalid GSTIN! The check digit validation has failed. " + + "Please ensure you've typed the GSTIN correctly.")) def get_itemised_tax_breakup_header(item_doctype, tax_accounts): if frappe.get_meta(item_doctype).has_field('gst_hsn_code'): From f99e013ebce325632615e2f8ba38737e70080b6d Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 10 Jan 2019 11:57:24 +0530 Subject: [PATCH 40/76] fix: gstin validation should work when there is no state (#16378) --- erpnext/regional/india/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 9f161afd57..fd0eb34abc 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -21,7 +21,9 @@ def validate_gstin_for_india(doc, method): validate_gstin_check_digit(doc.gstin) - if not doc.gst_state and doc.state: + if not doc.gst_state: + if not doc.state: + return state = doc.state.lower() states_lowercase = {s.lower():s for s in states} if state in states_lowercase: From 0d208851a4af29af77ee33df86a1e76969c539d0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 10 Jan 2019 17:56:11 +0530 Subject: [PATCH 41/76] Fix(stock-reco): Fixed codacy issues --- .../doctype/stock_reconciliation/test_stock_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 78ff915011..bc991b4e15 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -84,7 +84,7 @@ class TestStockReconciliation(unittest.TestCase): def test_get_items(self): create_warehouse("_Test Warehouse Group 1", {"is_group": 1}) create_warehouse("_Test Warehouse Ledger 1", {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"}) - item1 = make_item("_Test Stock Reco Item", {"default_warehouse": "_Test Warehouse Ledger 1 - _TC", + make_item("_Test Stock Reco Item", {"default_warehouse": "_Test Warehouse Ledger 1 - _TC", "is_stock_item": 1, "opening_stock": 100, "valuation_rate": 100}) items = get_items("_Test Warehouse Group 1 - _TC", nowdate(), nowtime()) From cd8908362a26f6ca672ddad5ac89f63c12cfef9e Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 11 Jan 2019 12:23:23 +0530 Subject: [PATCH 42/76] Changes Requested Done --- erpnext/projects/doctype/task/task.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 08abba31f4..371fc5c79b 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -46,9 +46,9 @@ class Task(NestedSet): if(self.project): if frappe.db.exists("Project", self.project): - doc = frappe.get_doc("Project", self.project) - if self.exp_end_date and doc.expected_end_date and getdate(self.exp_end_date) > getdate(doc.expected_end_date) : - frappe.throw(_("Expected end date cannot be after Project: '{0}' Expected end date").format(doc.name), EndDateCannotBeGreaterThanProjectEndDateError) + expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") + if self.exp_end_date and expected_end_date and getdate(self.exp_end_date) > getdate(expected_end_date) : + frappe.throw(_("Expected end date cannot be after Project: '{0}' Expected end date").format(self.project), EndDateCannotBeGreaterThanProjectEndDateError) def validate_status(self): if self.status!=self.get_db_value("status") and self.status == "Closed": From 7facc34851e1874cff436494b42645762b080f16 Mon Sep 17 00:00:00 2001 From: NahuelOperto Date: Fri, 11 Jan 2019 10:46:38 -0300 Subject: [PATCH 43/76] fix: removed filter from method validate --- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b2c6ccc7f1..a30f2fac3d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -141,7 +141,7 @@ class SalesOrder(SellingController): super(SalesOrder, self).validate_with_previous_doc({ "Quotation": { "ref_dn_field": "prevdoc_docname", - "compare_fields": [["company", "="], ["currency", "="]] + "compare_fields": [["company", "="]] } }) From ba54209c86c328568f33359af58e833051baa13d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 12 Jan 2019 17:58:00 +0530 Subject: [PATCH 44/76] [Fix] Exchange rate revaluation, get entries not working if accounts of the other currency than company currency not available (#16379) --- .../exchange_rate_revaluation.js | 2 ++ .../exchange_rate_revaluation.py | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js index c18057eb40..779cd6197e 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js @@ -39,6 +39,8 @@ frappe.ui.form.on('Exchange Rate Revaluation', { }); frm.events.get_total_gain_loss(frm); refresh_field("accounts"); + } else { + frappe.msgprint(__("No records found")); } } }); diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index ae77516eb7..cdfe34bead 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -67,17 +67,19 @@ class ExchangeRateRevaluation(Document): and account_currency != %s order by name""",(self.company, company_currency)) - account_details = frappe.db.sql(""" - select - account, party_type, party, account_currency, - sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency, - sum(debit) - sum(credit) as balance - from `tabGL Entry` - where account in (%s) - group by account, party_type, party - having sum(debit) != sum(credit) - order by account - """ % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1) + account_details = [] + if accounts: + account_details = frappe.db.sql(""" + select + account, party_type, party, account_currency, + sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency, + sum(debit) - sum(credit) as balance + from `tabGL Entry` + where account in (%s) + group by account, party_type, party + having sum(debit) != sum(credit) + order by account + """ % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1) return account_details From 4ed7cfc515fd50a99540ee4888f1595dd2d9a665 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 14 Jan 2019 17:14:39 +0530 Subject: [PATCH 45/76] tests(cost-center-company): Validate cost center's company and revent tests --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 5 ++--- .../stock/doctype/purchase_receipt/test_purchase_receipt.py | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 47e214e1b4..9cec1c0dc6 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -18,15 +18,14 @@ class GLEntry(Document): self.flags.ignore_submit_comment = True self.check_mandatory() self.validate_and_set_fiscal_year() + self.pl_must_have_cost_center() + self.validate_cost_center() if not self.flags.from_repost: - self.pl_must_have_cost_center() self.check_pl_account() - self.validate_cost_center() self.validate_party() self.validate_currency() - def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False): if not from_repost: self.validate_account_details(adv_adj) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 29caea156a..1207c5d128 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -325,7 +325,8 @@ def make_purchase_receipt(**args): "conversion_factor": args.conversion_factor or 1.0, "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", - "uom": args.uom or "_Test UOM" + "uom": args.uom or "_Test UOM", + "cost_center": "_Test Cost Center - _TC" }) if not args.do_not_save: From 9080221d46b74e78238ec8a170a68949b0b81aef Mon Sep 17 00:00:00 2001 From: NahuelOperto Date: Mon, 14 Jan 2019 12:12:56 -0300 Subject: [PATCH 46/76] fix: added test cases --- .../doctype/quotation/test_quotation.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index e79d46ae3f..909442af2f 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -31,6 +31,26 @@ class TestQuotation(unittest.TestCase): self.assertFalse(sales_order.get('payment_schedule')) + def test_make_sales_order_with_different_currency(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + + quotation = frappe.copy_doc(test_records[0]) + quotation.transaction_date = nowdate() + quotation.valid_till = add_months(quotation.transaction_date, 1) + quotation.insert() + quotation.submit() + + sales_order = make_sales_order(quotation.name) + sales_order.currency = "USD" + sales_order.conversion_rate = 20.0 + sales_order.delivery_date = "2019-01-01" + sales_order.naming_series = "_T-Quotation-" + sales_order.transaction_date = nowdate() + sales_order.insert() + + self.assertEquals(sales_order.currency, "USD") + self.assertNotEqual(sales_order.currency, quotation.currency) + def test_make_sales_order(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order From 627be1de51d5e79b3c2342118b10632eec78e1e6 Mon Sep 17 00:00:00 2001 From: Zlash65 Date: Tue, 15 Jan 2019 14:16:32 +0530 Subject: [PATCH 47/76] minor fixes for deferred calculations --- erpnext/stock/get_item_details.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 0e70338b21..cf45cc4607 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -273,7 +273,7 @@ def get_basic_details(args, item): "transaction_date": args.get("transaction_date") }) - if item.enable_deferred_revenue: + if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): out.update(calculate_service_end_date(args, item)) # calculate conversion factor @@ -310,9 +310,10 @@ def calculate_service_end_date(args, item=None): if not item: item = frappe.get_cached_doc("Item", args.item_code) - enable_deferred = "enable_deferred_revenue" if args.doctype=="Sales Invoice" else "enable_deferred_expense" - no_of_months = "no_of_months" if args.doctype=="Sales Invoice" else "no_of_months_exp" - account = "deferred_revenue_account" if args.doctype=="Sales Invoice" else "deferred_expense_account" + doctype = args.get("parenttype") or args.get("doctype") + enable_deferred = "enable_deferred_revenue" if doctype=="Sales Invoice" else "enable_deferred_expense" + no_of_months = "no_of_months" if doctype=="Sales Invoice" else "no_of_months_exp" + account = "deferred_revenue_account" if doctype=="Sales Invoice" else "deferred_expense_account" service_start_date = args.service_start_date if args.service_start_date else args.transaction_date service_end_date = add_months(service_start_date, item.get(no_of_months)) @@ -336,7 +337,7 @@ def get_default_expense_account(args, item, item_group): or args.expense_account) def get_default_deferred_account(args, item, fieldname=None): - if item.enable_deferred_revenue: + if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): return (item.get(fieldname) or args.get(fieldname) or frappe.get_cached_value('Company', args.company, "default_"+fieldname)) From 68ea608ca8c3dfd439138ce0fbc359020928783e Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 15 Jan 2019 14:47:14 +0530 Subject: [PATCH 48/76] fix: import unicode_literals --- erpnext/patches.txt | 2 +- erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py | 1 + erpnext/setup/default_success_action.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index be8201a07d..7bf63a2290 100755 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -1,9 +1,9 @@ execute:import unidecode # new requirement erpnext.patches.v8_0.move_perpetual_inventory_setting +erpnext.patches.v11_0.rename_production_order_to_work_order erpnext.patches.v11_0.refactor_naming_series erpnext.patches.v11_0.refactor_autoname_naming erpnext.patches.v10_0.rename_schools_to_education -erpnext.patches.v11_0.rename_production_order_to_work_order erpnext.patches.v4_0.validate_v3_patch erpnext.patches.v4_0.fix_employee_user_id erpnext.patches.v4_0.remove_employee_role_if_no_employee diff --git a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py index 83130966d4..52d4621c7b 100644 --- a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py +++ b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import frappe from frappe.model.rename_doc import rename_doc from frappe.model.utils.rename_field import rename_field diff --git a/erpnext/setup/default_success_action.py b/erpnext/setup/default_success_action.py index e8494a1f96..6c2a97a89c 100644 --- a/erpnext/setup/default_success_action.py +++ b/erpnext/setup/default_success_action.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from frappe import _ doctype_list = [ From 876d868fa13aa03ed1c3adf3b017264537165033 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 15 Jan 2019 15:45:33 +0530 Subject: [PATCH 49/76] fix(tests): change patch order, optimise travis --- .travis.yml | 11 +++++------ erpnext/patches.txt | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 448cd40a39..e21d59511a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,17 +30,16 @@ before_script: - cd ~/frappe-bench - bench get-app erpnext $TRAVIS_BUILD_DIR - bench use test_site - - bench reinstall --yes - - bench build - - bench scheduler disable - - sed -i 's/9000/9001/g' sites/common_site_config.json - - bench start & - - sleep 10 jobs: include: - stage: test script: + - bench reinstall --yes + - bench scheduler disable + - sed -i 's/9000/9001/g' sites/common_site_config.json + - bench start & + - sleep 10 - set -e - bench run-tests env: Server Side Test diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bc6f99db84..9d5b331123 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -1,5 +1,6 @@ execute:import unidecode # new requirement erpnext.patches.v8_0.move_perpetual_inventory_setting +erpnext.patches.v8_9.set_print_zero_amount_taxes erpnext.patches.v10_0.rename_schools_to_education erpnext.patches.v4_0.validate_v3_patch erpnext.patches.v4_0.fix_employee_user_id @@ -442,7 +443,6 @@ erpnext.patches.v8_9.add_setup_progress_actions #08-09-2017 #26-09-2017 #22-11-2 erpnext.patches.v8_9.rename_company_sales_target_field erpnext.patches.v8_8.set_bom_rate_as_per_uom erpnext.patches.v8_8.add_new_fields_in_accounts_settings -erpnext.patches.v8_9.set_print_zero_amount_taxes erpnext.patches.v8_9.set_default_customer_group erpnext.patches.v8_9.remove_employee_from_salary_structure_parent erpnext.patches.v8_9.delete_gst_doctypes_for_outside_india_accounts From a3dd798badb176bd39c677e2cf3b8edeb3da58a3 Mon Sep 17 00:00:00 2001 From: Zlash65 Date: Tue, 15 Jan 2019 14:36:11 +0530 Subject: [PATCH 50/76] clean if conditions --- erpnext/stock/get_item_details.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index cf45cc4607..bbea9043a3 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -311,9 +311,14 @@ def calculate_service_end_date(args, item=None): item = frappe.get_cached_doc("Item", args.item_code) doctype = args.get("parenttype") or args.get("doctype") - enable_deferred = "enable_deferred_revenue" if doctype=="Sales Invoice" else "enable_deferred_expense" - no_of_months = "no_of_months" if doctype=="Sales Invoice" else "no_of_months_exp" - account = "deferred_revenue_account" if doctype=="Sales Invoice" else "deferred_expense_account" + if doctype == "Sales Invoice": + enable_deferred = "enable_deferred_revenue" + no_of_months = "no_of_months" + account = "deferred_revenue_account" + else: + enable_deferred = "enable_deferred_expense" + no_of_months = "no_of_months_exp" + account = "deferred_expense_account" service_start_date = args.service_start_date if args.service_start_date else args.transaction_date service_end_date = add_months(service_start_date, item.get(no_of_months)) From cef56b514719a7ef2b0d216bdb757a5645911a51 Mon Sep 17 00:00:00 2001 From: Zlash65 Date: Tue, 15 Jan 2019 16:26:58 +0530 Subject: [PATCH 51/76] fixes asset category field as link type and read only --- erpnext/assets/doctype/asset/asset.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 5296e22492..6b3c3cc73d 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 1, @@ -154,7 +155,7 @@ "columns": 0, "fetch_from": "item_code.asset_category", "fieldname": "asset_category", - "fieldtype": "Read Only", + "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -165,12 +166,12 @@ "label": "Asset Category", "length": 0, "no_copy": 0, - "options": "", + "options": "Asset Category", "permlevel": 0, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 0, + "read_only": 1, "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, @@ -1881,7 +1882,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 14:44:24.507215", + "modified": "2019-01-15 16:12:48.314196", "modified_by": "Administrator", "module": "Assets", "name": "Asset", From 529cc1ca51ed556a9ec2f4dbc3268b08a29fa486 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 15 Jan 2019 19:46:36 +0600 Subject: [PATCH 52/76] bumped to version 10.1.77 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index b9d86a8e84..26755dde61 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '10.1.76' +__version__ = '10.1.77' def get_default_company(user=None): '''Get default company for user''' From fccb1e55ec01f80764bf656e75bac370b5e76db7 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Wed, 16 Jan 2019 14:36:55 +0500 Subject: [PATCH 53/76] fix(Gross Profit Report): corrected fieldnames in return invoice query --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2ed664c9bf..4d455da994 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -184,7 +184,7 @@ class GrossProfitGenerator(object): def get_returned_invoice_items(self): returned_invoices = frappe.db.sql(""" select - si.name, si_item.item_code, si_item.qty, si_item.base_amount, si.return_against + si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against from `tabSales Invoice` si, `tabSales Invoice Item` si_item where From 34766c0c50ec5bb69d4baab2824b6d3b54792cbd Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Thu, 20 Dec 2018 18:50:24 +0530 Subject: [PATCH 54/76] BIS issue fixes --- erpnext/controllers/buying_controller.py | 4 +- .../production_plan/production_plan.py | 112 +++++++++++++----- .../doctype/sales_order/sales_order.js | 4 +- .../doctype/sales_order/sales_order.py | 37 ++++-- 4 files changed, 113 insertions(+), 44 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 541e56d781..664bce4e4f 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -678,7 +678,9 @@ class BuyingController(StockController): frappe.db.sql("delete from `tabSerial No` where purchase_document_no=%s", self.name) def validate_schedule_date(self): - if not self.schedule_date and self.get("items"): + if not self.get("items"): + return + if not self.schedule_date: self.schedule_date = min([d.schedule_date for d in self.get("items")]) if self.schedule_date: diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 9bf3858167..046377ec76 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -7,7 +7,7 @@ import frappe, json from frappe import msgprint, _ from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no -from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime +from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from six import string_types from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults @@ -376,13 +376,16 @@ def get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom, ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, item.item_name, bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, - item.default_material_request_type, item.min_order_qty, item_default.default_warehouse + item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, + item.purchase_uom, item_uom.conversion_factor from `tabBOM Explosion Item` bei JOIN `tabBOM` bom ON bom.name = bei.parent JOIN `tabItem` item ON item.name = bei.item_code LEFT JOIN `tabItem Default` item_default ON item_default.parent = item.name and item_default.company=%s + LEFT JOIN `tabUOM Conversion Detail` item_uom + ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom where bei.docstatus < 2 and bom.name=%s and item.is_stock_item in (1, {0}) @@ -399,13 +402,15 @@ def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_ item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, item.default_bom as default_bom, bom_item.description as description, bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, - item_default.default_warehouse + item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor FROM `tabBOM Item` bom_item JOIN `tabBOM` bom ON bom.name = bom_item.parent JOIN tabItem item ON bom_item.item_code = item.name LEFT JOIN `tabItem Default` item_default ON item.name = item_default.parent and item_default.company = %(company)s + LEFT JOIN `tabUOM Conversion Detail` item_uom + ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom where bom.name = %(bom)s and bom_item.docstatus < 2 @@ -431,17 +436,30 @@ def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_ return bom_wise_item_details def add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, row, data, warehouse, company): + print("add item in material request") total_qty = row.qty * planned_qty projected_qty, actual_qty = get_bin_details(row) requested_qty = 0 if ignore_existing_ordered_qty: requested_qty = total_qty - else: + elif total_qty > projected_qty: requested_qty = total_qty - projected_qty if requested_qty > 0 and requested_qty < row.min_order_qty: requested_qty = row.min_order_qty item_group_defaults = get_item_group_defaults(item, company) + + if not row.purchase_uom: + row.purchase_uom = row.stock_uom + + if row.purchase_uom != row.stock_uom: + if not row.conversion_factor: + frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format(row.purchase_uom, row.stock_uom, item)) + + requested_qty = requested_qty / row.conversion_factor + if frappe.db.get_value("UOM", row.purchase_uom, "must_be_whole_number"): + requested_qty = ceil(requested_qty) + print(row) if requested_qty > 0: doc.setdefault('mr_items', []).append({ 'item_code': item, @@ -487,8 +505,8 @@ def get_sales_orders(self): "project": self.project, "item": self.item_code, "company": self.company - }, as_dict=1) + }, as_dict=1) return open_so @frappe.whitelist() @@ -511,6 +529,7 @@ def get_bin_details(row): @frappe.whitelist() def get_items_for_material_requests(doc, company=None): + print("get items for material request") if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) @@ -520,34 +539,69 @@ def get_items_for_material_requests(doc, company=None): for data in po_items: warehouse = None bom_wise_item_details = {} - - if data.get('required_qty'): - planned_qty = data.get('required_qty') - bom_no = data.get('bom') - ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') - include_non_stock_items = 1 - warehouse = data.get('for_warehouse') - if data.get('include_exploded_items'): - include_subcontracted_items = 1 + if data.get("bom"): + print(doc),print("-------------------------------------------------") + if data.get('required_qty'): + planned_qty = data.get('required_qty') + bom_no = data.get('bom') + ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') + include_non_stock_items = 1 + warehouse = data.get('for_warehouse') + if data.get('include_exploded_items'): + include_subcontracted_items = 1 + else: + include_subcontracted_items = 0 else: - include_subcontracted_items = 0 - else: - planned_qty = data.get('planned_qty') - bom_no = data.get('bom_no') - include_subcontracted_items = doc.get('include_subcontracted_items') - company = doc.get('company') - include_non_stock_items = doc.get('include_non_stock_items') - ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty') - if not planned_qty: - frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx'))) + planned_qty = data.get('planned_qty') + bom_no = data.get('bom_no') + include_subcontracted_items = doc.get('include_subcontracted_items') + company = doc.get('company') + include_non_stock_items = doc.get('include_non_stock_items') + ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty') + if not planned_qty: + frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx'))) - if data.get('include_exploded_items') and bom_no and include_subcontracted_items: - # fetch exploded items from BOM - bom_wise_item_details = get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items) + if data.get('include_exploded_items') and bom_no and include_subcontracted_items: + # fetch exploded items from BOM + bom_wise_item_details = get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items) + else: + bom_wise_item_details = get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, 1) + for item, item_details in bom_wise_item_details.items(): + print(item),print(item_details) + if item_details.qty > 0: + add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, item_details, data, warehouse, company) else: - bom_wise_item_details = get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, 1) - for item, item_details in bom_wise_item_details.items(): + sales_order_item = frappe.get_doc('Sales OrderItem', data.sales_order_item).as_dict() + planned_qty = data.get('required_qty') + ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') + item = doc.item_code + purchase_uom = sales_order_item.uom + stock_uom = sales_order_item.stock_uom + conversion_factor = sales_order_item.conversion_factor + qty = doc.required_qty + if not purchase_uom == stock_uom: + qty = qty / conversion_factor + if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): + qty = ceil(qty) + item_details = { + 'item_name' = sales_order_item.item_name, + 'default_bom' = doc.bom, + 'purchase_uom' = purchase_uom, + 'default_warehouse' = doc.warehouse, + 'min_order_qty' = + 'default_material_request_type' = + 'qty' = qty, + 'is_sub_contracted' = , + 'item_code' = doc.item_code, + 'description' = sales_order_item.description, + 'stock_uom' = stock_uom, + 'conversion_factor' = conversion_factor, + 'source_warehouse' = , + } + warehouse = doc.warehouse + company = if item_details.qty > 0: add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, item_details, data, warehouse, company) + print(doc),print("-------------------------------------------------") return doc['mr_items'] diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 54d7654c2d..ab2f6ee8e3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -344,6 +344,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( label: __('Include Exploded Items')}, {fieldtype:'Check', fieldname:'ignore_existing_ordered_qty', label: __('Ignore Existing Ordered Qty')}, + {fieldtype:'Check', fieldname:'include_raw_materials_from_sales_order', + label: __('Include raw materials from sales order')}, { fieldtype:'Table', fieldname: 'items', description: __('Select BOM, Qty and For Warehouse'), @@ -367,7 +369,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } ] var d = new frappe.ui.Dialog({ - title: __("Select from Items having BOM"), + title: __("Items for Raw Material Request"), fields: fields, primary_action: function() { var data = d.get_values(); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 229f4f6837..1460bc8b69 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -341,11 +341,11 @@ class SalesOrder(SellingController): delivered_qty += item.delivered_qty tot_qty += item.qty - + if tot_qty != 0: self.db_set("per_delivered", flt(delivered_qty/tot_qty) * 100, update_modified=False) - + def set_indicator(self): """Set indicator for portal""" @@ -372,20 +372,19 @@ class SalesOrder(SellingController): def get_work_order_items(self, for_raw_material_request=0): '''Returns items with BOM that already do not have a linked work order''' items = [] - for table in [self.items, self.packed_items]: for i in table: bom = get_default_bom_item(i.item_code) - if bom: - stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty - if not for_raw_material_request: - total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order` - where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2''', (i.item_code, self.name, i.name))[0][0]) - pending_qty = stock_qty - total_work_order_qty - else: - pending_qty = stock_qty + stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty + if not for_raw_material_request: + total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order` + where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2''', (i.item_code, self.name, i.name))[0][0]) + pending_qty = stock_qty - total_work_order_qty + else: + pending_qty = stock_qty - if pending_qty: + if pending_qty: + if bom: items.append(dict( name= i.name, item_code= i.item_code, @@ -395,6 +394,16 @@ class SalesOrder(SellingController): required_qty = pending_qty if for_raw_material_request else 0, sales_order_item = i.name )) + else: + items.append(dict( + name= i.name, + item_code= i.item_code, + bom = '', + warehouse = i.warehouse, + pending_qty = pending_qty, + required_qty = pending_qty if for_raw_material_request else 0, + sales_order_item = i.name + )) return items def on_recurring(self, reference_doc, auto_repeat_doc): @@ -923,10 +932,12 @@ def make_raw_material_request(items, company, sales_order, project=None): for item in items.get('items'): item["include_exploded_items"] = items.get('include_exploded_items') item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty') + item["include_raw_materials_from_sales_order"] = items.get('include_raw_materials_from_sales_order') raw_materials = get_items_for_material_requests(items, company) if not raw_materials: frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available.")) + return material_request = frappe.new_doc('Material Request') material_request.update(dict( @@ -951,4 +962,4 @@ def make_raw_material_request(items, company, sales_order, project=None): material_request.flags.ignore_permissions = 1 material_request.run_method("set_missing_values") material_request.submit() - return material_request \ No newline at end of file + return material_request From 693e81db48a0a9315fa27ae4da291af31085c262 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Fri, 21 Dec 2018 17:56:28 +0530 Subject: [PATCH 55/76] more bis fixes --- .../production_plan/production_plan.py | 99 +++++++++---------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 046377ec76..f57a9112bc 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -436,8 +436,7 @@ def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_ return bom_wise_item_details def add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, row, data, warehouse, company): - print("add item in material request") - total_qty = row.qty * planned_qty + total_qty = row['qty'] * planned_qty projected_qty, actual_qty = get_bin_details(row) requested_qty = 0 @@ -445,29 +444,27 @@ def add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered requested_qty = total_qty elif total_qty > projected_qty: requested_qty = total_qty - projected_qty - if requested_qty > 0 and requested_qty < row.min_order_qty: - requested_qty = row.min_order_qty + if requested_qty > 0 and requested_qty < row['min_order_qty']: + requested_qty = row['min_order_qty'] item_group_defaults = get_item_group_defaults(item, company) - if not row.purchase_uom: - row.purchase_uom = row.stock_uom + if not row['purchase_uom']: + row['purchase_uom'] = row['stock_uom'] - if row.purchase_uom != row.stock_uom: - if not row.conversion_factor: - frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format(row.purchase_uom, row.stock_uom, item)) - - requested_qty = requested_qty / row.conversion_factor - if frappe.db.get_value("UOM", row.purchase_uom, "must_be_whole_number"): + if row['purchase_uom'] != row['stock_uom']: + if not row['conversion_factor']: + frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format(row['purchase_uom'], row['stock_uom'], item)) + requested_qty = requested_qty / row['conversion_factor'] + if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"): requested_qty = ceil(requested_qty) - print(row) if requested_qty > 0: doc.setdefault('mr_items', []).append({ 'item_code': item, - 'item_name': row.item_name, + 'item_name': row['item_name'], 'quantity': requested_qty, - 'warehouse': warehouse or row.source_warehouse or row.default_warehouse or item_group_defaults.get("default_warehouse"), + 'warehouse': warehouse or row.get('source_warehouse') or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"), 'actual_qty': actual_qty, - 'min_order_qty': row.min_order_qty, + 'min_order_qty': row['min_order_qty'], 'sales_order': data.get('sales_order') }) @@ -515,7 +512,7 @@ def get_bin_details(row): row = frappe._dict(json.loads(row)) conditions = "" - warehouse = row.source_warehouse or row.default_warehouse or row.warehouse + warehouse = row.get('source_warehouse') or row.get('default_warehouse') if warehouse: lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) conditions = " and exists(select name from `tabWarehouse` where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse)".format(lft, rgt) @@ -523,13 +520,12 @@ def get_bin_details(row): item_projected_qty = frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, ifnull(sum(actual_qty),0) as actual_qty from `tabBin` where item_code = %(item_code)s {conditions} - """.format(conditions=conditions), { "item_code": row.item_code }, as_list=1) + """.format(conditions=conditions), { "item_code": row['item_code'] }, as_list=1) return item_projected_qty and item_projected_qty[0] or (0,0) @frappe.whitelist() def get_items_for_material_requests(doc, company=None): - print("get items for material request") if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) @@ -540,7 +536,6 @@ def get_items_for_material_requests(doc, company=None): warehouse = None bom_wise_item_details = {} if data.get("bom"): - print(doc),print("-------------------------------------------------") if data.get('required_qty'): planned_qty = data.get('required_qty') bom_no = data.get('bom') @@ -565,43 +560,39 @@ def get_items_for_material_requests(doc, company=None): # fetch exploded items from BOM bom_wise_item_details = get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items) else: - bom_wise_item_details = get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, 1) + bom_wise_item_details = get_subitems(doc, data, bom_wise_item_details, bom_no, company, + include_non_stock_items, include_subcontracted_items, 1) for item, item_details in bom_wise_item_details.items(): - print(item),print(item_details) if item_details.qty > 0: - add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, item_details, data, warehouse, company) + add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, + item, item_details, data, warehouse, company) else: - sales_order_item = frappe.get_doc('Sales OrderItem', data.sales_order_item).as_dict() + item_master = frappe.get_doc('Item', data['item_code']).as_dict() planned_qty = data.get('required_qty') - ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') - item = doc.item_code - purchase_uom = sales_order_item.uom - stock_uom = sales_order_item.stock_uom - conversion_factor = sales_order_item.conversion_factor - qty = doc.required_qty - if not purchase_uom == stock_uom: - qty = qty / conversion_factor - if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): - qty = ceil(qty) - item_details = { - 'item_name' = sales_order_item.item_name, - 'default_bom' = doc.bom, - 'purchase_uom' = purchase_uom, - 'default_warehouse' = doc.warehouse, - 'min_order_qty' = - 'default_material_request_type' = - 'qty' = qty, - 'is_sub_contracted' = , - 'item_code' = doc.item_code, - 'description' = sales_order_item.description, - 'stock_uom' = stock_uom, - 'conversion_factor' = conversion_factor, - 'source_warehouse' = , - } - warehouse = doc.warehouse - company = - if item_details.qty > 0: - add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, item_details, data, warehouse, company) - print(doc),print("-------------------------------------------------") + + purchase_uom = item_master.purchase_uom or item_master.stock_uom + conversion_factor = 0 + for d in item_master.get("uoms"): + if d.uom == purchase_uom: + conversion_factor = d.conversion_factor + + item_details = frappe._dict({ + 'item_name' : item_master.item_name, + 'default_bom' : doc.bom, + 'purchase_uom' : purchase_uom, + 'default_warehouse': item_master.default_warehouse, + 'min_order_qty' : item_master.min_order_qty, + 'default_material_request_type' : item_master.default_material_request_type, + 'qty': 1, + 'is_sub_contracted' : item_master.is_subcontracted_item, + 'item_code' : item_master.name, + 'description' : item_master.description, + 'stock_uom' : item_master.stock_uom, + 'conversion_factor' : conversion_factor, + }) + + if item_details['qty'] > 0: + add_item_in_material_request_items(doc, planned_qty, data.get('ignore_existing_ordered_qty'), item_master.name, + item_details, data, data.get('for_warehouse'), company) return doc['mr_items'] From 3dde94941902e35fdafb1532486c940fe6d1f2ed Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 27 Dec 2018 14:24:08 +0530 Subject: [PATCH 56/76] feat(mr_against_so): Merge same items from different BOMs --- .../production_plan/production_plan.py | 149 ++--- .../production_planning_tool.py | 552 ------------------ .../doctype/sales_order/sales_order.js | 2 - .../doctype/sales_order/sales_order.py | 2 +- 4 files changed, 84 insertions(+), 621 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f57a9112bc..1ad6e64d41 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -9,7 +9,7 @@ from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil from erpnext.manufacturing.doctype.work_order.work_order import get_item_details -from six import string_types +from six import string_types, iteritems from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults class ProductionPlan(Document): @@ -372,9 +372,9 @@ class ProductionPlan(Document): else : msgprint(_("No material request created")) -def get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items): +def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1): for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom, - ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, item.item_name, + ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor @@ -390,15 +390,16 @@ def get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock bei.docstatus < 2 and bom.name=%s and item.is_stock_item in (1, {0}) group by bei.item_code, bei.stock_uom""".format(0 if include_non_stock_items else 1), - (company, bom_no), as_dict=1): - bom_wise_item_details.setdefault(d.get('item_code'), d) - return bom_wise_item_details + (planned_qty, company, bom_no), as_dict=1): + item_details.setdefault(d.get('item_code'), d) + return item_details -def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, parent_qty): +def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_items, + include_subcontracted_items, parent_qty, planned_qty=1): items = frappe.db.sql(""" SELECT bom_item.item_code, default_material_request_type, item.item_name, - ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, + ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty, item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, item.default_bom as default_bom, bom_item.description as description, bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, @@ -418,25 +419,27 @@ def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_ group by bom_item.item_code""".format(0 if include_non_stock_items else 1),{ 'bom': bom_no, 'parent_qty': parent_qty, + 'planned_qty': planned_qty, 'company': company }, as_dict=1) for d in items: if not data.get('include_exploded_items') or not d.default_bom: - if d.item_code in bom_wise_item_details: - bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty + if d.item_code in item_details: + item_details[d.item_code].qty = item_details[d.item_code].qty + d.qty else: - bom_wise_item_details[d.item_code] = d + item_details[d.item_code] = d if data.get('include_exploded_items') and d.default_bom: if ((d.default_material_request_type in ["Manufacture", "Purchase"] and not d.is_sub_contracted) or (d.is_sub_contracted and include_subcontracted_items)): if d.qty > 0: - get_subitems(doc, data, bom_wise_item_details, d.default_bom, company, include_non_stock_items, include_subcontracted_items, d.qty) - return bom_wise_item_details + get_subitems(doc, data, item_details, d.default_bom, company, + include_non_stock_items, include_subcontracted_items, d.qty) + return item_details -def add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, row, data, warehouse, company): - total_qty = row['qty'] * planned_qty +def get_material_request_items(row, sales_order, company, ignore_existing_ordered_qty, warehouse): + total_qty = row['qty'] projected_qty, actual_qty = get_bin_details(row) requested_qty = 0 @@ -446,27 +449,31 @@ def add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered requested_qty = total_qty - projected_qty if requested_qty > 0 and requested_qty < row['min_order_qty']: requested_qty = row['min_order_qty'] - item_group_defaults = get_item_group_defaults(item, company) + item_group_defaults = get_item_group_defaults(row.item_code, company) if not row['purchase_uom']: row['purchase_uom'] = row['stock_uom'] if row['purchase_uom'] != row['stock_uom']: if not row['conversion_factor']: - frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format(row['purchase_uom'], row['stock_uom'], item)) + frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}") + .format(row['purchase_uom'], row['stock_uom'], item)) requested_qty = requested_qty / row['conversion_factor'] + if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"): requested_qty = ceil(requested_qty) + if requested_qty > 0: - doc.setdefault('mr_items', []).append({ - 'item_code': item, - 'item_name': row['item_name'], + return { + 'item_code': row.item_code, + 'item_name': row.item_name, 'quantity': requested_qty, - 'warehouse': warehouse or row.get('source_warehouse') or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"), + 'warehouse': warehouse or row.get('source_warehouse') \ + or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"), 'actual_qty': actual_qty, 'min_order_qty': row['min_order_qty'], - 'sales_order': data.get('sales_order') - }) + 'sales_order': sales_order + } def get_sales_orders(self): so_filter = item_filter = "" @@ -525,74 +532,84 @@ def get_bin_details(row): return item_projected_qty and item_projected_qty[0] or (0,0) @frappe.whitelist() -def get_items_for_material_requests(doc, company=None): +def get_items_for_material_requests(doc, sales_order=None, company=None): if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) doc['mr_items'] = [] po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') + company = doc.get('company') + so_item_details = frappe._dict() for data in po_items: - warehouse = None - bom_wise_item_details = {} + warehouse = data.get('for_warehouse') + ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or doc.get('ignore_existing_ordered_qty') + planned_qty = data.get('required_qty') or data.get('planned_qty') + item_details = {} if data.get("bom"): if data.get('required_qty'): - planned_qty = data.get('required_qty') bom_no = data.get('bom') - ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') include_non_stock_items = 1 - warehouse = data.get('for_warehouse') - if data.get('include_exploded_items'): - include_subcontracted_items = 1 - else: - include_subcontracted_items = 0 + include_subcontracted_items = 1 if data.get('include_exploded_items') else 0 else: - planned_qty = data.get('planned_qty') bom_no = data.get('bom_no') include_subcontracted_items = doc.get('include_subcontracted_items') - company = doc.get('company') include_non_stock_items = doc.get('include_non_stock_items') - ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty') + if not planned_qty: frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx'))) - if data.get('include_exploded_items') and bom_no and include_subcontracted_items: - # fetch exploded items from BOM - bom_wise_item_details = get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items) - else: - bom_wise_item_details = get_subitems(doc, data, bom_wise_item_details, bom_no, company, - include_non_stock_items, include_subcontracted_items, 1) - for item, item_details in bom_wise_item_details.items(): - if item_details.qty > 0: - add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, - item, item_details, data, warehouse, company) + if bom_no: + if data.get('include_exploded_items') and include_subcontracted_items: + # fetch exploded items from BOM + item_details = get_exploded_items(item_details, + company, bom_no,include_non_stock_items, planned_qty=planned_qty) + else: + item_details = get_subitems(doc, data, item_details, bom_no, company, + include_non_stock_items, include_subcontracted_items, 1, planned_qty=planned_qty) else: item_master = frappe.get_doc('Item', data['item_code']).as_dict() - planned_qty = data.get('required_qty') - purchase_uom = item_master.purchase_uom or item_master.stock_uom conversion_factor = 0 for d in item_master.get("uoms"): if d.uom == purchase_uom: conversion_factor = d.conversion_factor - item_details = frappe._dict({ - 'item_name' : item_master.item_name, - 'default_bom' : doc.bom, - 'purchase_uom' : purchase_uom, - 'default_warehouse': item_master.default_warehouse, - 'min_order_qty' : item_master.min_order_qty, - 'default_material_request_type' : item_master.default_material_request_type, - 'qty': 1, - 'is_sub_contracted' : item_master.is_subcontracted_item, - 'item_code' : item_master.name, - 'description' : item_master.description, - 'stock_uom' : item_master.stock_uom, - 'conversion_factor' : conversion_factor, - }) + item_details[item_master.name] = frappe._dict( + { + 'item_name' : item_master.item_name, + 'default_bom' : doc.bom, + 'purchase_uom' : purchase_uom, + 'default_warehouse': item_master.default_warehouse, + 'min_order_qty' : item_master.min_order_qty, + 'default_material_request_type' : item_master.default_material_request_type, + 'qty': planned_qty or 1, + 'is_sub_contracted' : item_master.is_subcontracted_item, + 'item_code' : item_master.name, + 'description' : item_master.description, + 'stock_uom' : item_master.stock_uom, + 'conversion_factor' : conversion_factor, + } + ) - if item_details['qty'] > 0: - add_item_in_material_request_items(doc, planned_qty, data.get('ignore_existing_ordered_qty'), item_master.name, - item_details, data, data.get('for_warehouse'), company) + if not sales_order: + sales_order = doc.get("sales_order") - return doc['mr_items'] + for item_code, details in iteritems(item_details): + so_item_details.setdefault(sales_order, frappe._dict()) + if item_code in so_item_details.get(sales_order, {}): + so_item_details[sales_order][item_code]['qty'] = so_item_details[sales_order][item_code].get("qty", 0) + flt(details.qty) + else: + so_item_details[sales_order][item_code] = details + + mr_items = [] + for sales_order, item_code in iteritems(so_item_details): + item_dict = so_item_details[sales_order] + for details in item_dict.values(): + if details.qty > 0: + items = get_material_request_items(details, sales_order, company, + ignore_existing_ordered_qty, warehouse) + if items: + mr_items.append(items) + + return mr_items diff --git a/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py b/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py deleted file mode 100644 index 323aaf9d62..0000000000 --- a/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +++ /dev/null @@ -1,552 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe -from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and - -from frappe import msgprint, _ - -from frappe.model.document import Document -from erpnext.manufacturing.doctype.bom.bom import validate_bom_no -from erpnext.manufacturing.doctype.work_order.work_order import get_item_details - -class ProductionPlanningTool(Document): - def clear_table(self, table_name): - self.set(table_name, []) - - def validate_company(self): - if not self.company: - frappe.throw(_("Please enter Company")) - - def get_open_sales_orders(self): - """ Pull sales orders which are pending to deliver based on criteria selected""" - so_filter = item_filter = "" - if self.from_date: - so_filter += " and so.transaction_date >= %(from_date)s" - if self.to_date: - so_filter += " and so.transaction_date <= %(to_date)s" - if self.customer: - so_filter += " and so.customer = %(customer)s" - if self.project: - so_filter += " and so.project = %(project)s" - - if self.fg_item: - item_filter += " and so_item.item_code = %(item)s" - - open_so = frappe.db.sql(""" - select distinct so.name, so.transaction_date, so.customer, so.base_grand_total - from `tabSales Order` so, `tabSales Order Item` so_item - where so_item.parent = so.name - and so.docstatus = 1 and so.status not in ("Stopped", "Closed") - and so.company = %(company)s - and so_item.qty > so_item.delivered_qty {0} {1} - and (exists (select name from `tabBOM` bom where bom.item=so_item.item_code - and bom.is_active = 1) - or exists (select name from `tabPacked Item` pi - where pi.parent = so.name and pi.parent_item = so_item.item_code - and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1))) - """.format(so_filter, item_filter), { - "from_date": self.from_date, - "to_date": self.to_date, - "customer": self.customer, - "project": self.project, - "item": self.fg_item, - "company": self.company - }, as_dict=1) - - self.add_so_in_table(open_so) - - def add_so_in_table(self, open_so): - """ Add sales orders in the table""" - self.clear_table("sales_orders") - - so_list = [] - for r in open_so: - if cstr(r['name']) not in so_list: - pp_so = self.append('sales_orders', {}) - pp_so.sales_order = r['name'] - pp_so.sales_order_date = cstr(r['transaction_date']) - pp_so.customer = cstr(r['customer']) - pp_so.grand_total = flt(r['base_grand_total']) - - def get_pending_material_requests(self): - """ Pull Material Requests that are pending based on criteria selected""" - mr_filter = item_filter = "" - if self.from_date: - mr_filter += " and mr.transaction_date >= %(from_date)s" - if self.to_date: - mr_filter += " and mr.transaction_date <= %(to_date)s" - if self.warehouse: - mr_filter += " and mr_item.warehouse = %(warehouse)s" - - if self.fg_item: - item_filter += " and mr_item.item_code = %(item)s" - - pending_mr = frappe.db.sql(""" - select distinct mr.name, mr.transaction_date - from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item - where mr_item.parent = mr.name - and mr.material_request_type = "Manufacture" - and mr.docstatus = 1 - and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1} - and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1)) - """.format(mr_filter, item_filter), { - "from_date": self.from_date, - "to_date": self.to_date, - "warehouse": self.warehouse, - "item": self.fg_item - }, as_dict=1) - - self.add_mr_in_table(pending_mr) - - def add_mr_in_table(self, pending_mr): - """ Add Material Requests in the table""" - self.clear_table("material_requests") - - mr_list = [] - for r in pending_mr: - if cstr(r['name']) not in mr_list: - mr = self.append('material_requests', {}) - mr.material_request = r['name'] - mr.material_request_date = cstr(r['transaction_date']) - - def get_items(self): - if self.get_items_from == "Sales Order": - self.get_so_items() - elif self.get_items_from == "Material Request": - self.get_mr_items() - - def get_so_items(self): - so_list = [d.sales_order for d in self.get('sales_orders') if d.sales_order] - if not so_list: - msgprint(_("Please enter Sales Orders in the above table")) - return [] - - item_condition = "" - if self.fg_item: - item_condition = ' and so_item.item_code = "{0}"'.format(frappe.db.escape(self.fg_item)) - - items = frappe.db.sql("""select distinct parent, item_code, warehouse, - (qty - delivered_qty)*conversion_factor as pending_qty - from `tabSales Order Item` so_item - where parent in (%s) and docstatus = 1 and qty > delivered_qty - and exists (select name from `tabBOM` bom where bom.item=so_item.item_code - and bom.is_active = 1) %s""" % \ - (", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1) - - if self.fg_item: - item_condition = ' and pi.item_code = "{0}"'.format(frappe.db.escape(self.fg_item)) - - packed_items = frappe.db.sql("""select distinct pi.parent, pi.item_code, pi.warehouse as warehouse, - (((so_item.qty - so_item.delivered_qty) * pi.qty) / so_item.qty) - as pending_qty - from `tabSales Order Item` so_item, `tabPacked Item` pi - where so_item.parent = pi.parent and so_item.docstatus = 1 - and pi.parent_item = so_item.item_code - and so_item.parent in (%s) and so_item.qty > so_item.delivered_qty - and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1) %s""" % \ - (", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1) - - self.add_items(items + packed_items) - - def get_mr_items(self): - mr_list = [d.material_request for d in self.get('material_requests') if d.material_request] - if not mr_list: - msgprint(_("Please enter Material Requests in the above table")) - return [] - - item_condition = "" - if self.fg_item: - item_condition = ' and mr_item.item_code = "' + frappe.db.escape(self.fg_item, percent=False) + '"' - - items = frappe.db.sql("""select distinct parent, name, item_code, warehouse, - (qty - ordered_qty) as pending_qty - from `tabMaterial Request Item` mr_item - where parent in (%s) and docstatus = 1 and qty > ordered_qty - and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1) %s""" % \ - (", ".join(["%s"] * len(mr_list)), item_condition), tuple(mr_list), as_dict=1) - - self.add_items(items) - - - def add_items(self, items): - self.clear_table("items") - for p in items: - item_details = get_item_details(p['item_code']) - pi = self.append('items', {}) - pi.warehouse = p['warehouse'] - pi.item_code = p['item_code'] - pi.description = item_details and item_details.description or '' - pi.stock_uom = item_details and item_details.stock_uom or '' - pi.bom_no = item_details and item_details.bom_no or '' - pi.planned_qty = flt(p['pending_qty']) - pi.pending_qty = flt(p['pending_qty']) - - if self.get_items_from == "Sales Order": - pi.sales_order = p['parent'] - elif self.get_items_from == "Material Request": - pi.material_request = p['parent'] - pi.material_request_item = p['name'] - - def validate_data(self): - self.validate_company() - for d in self.get('items'): - if not d.bom_no: - frappe.throw(_("Please select BOM for Item in Row {0}".format(d.idx))) - else: - validate_bom_no(d.item_code, d.bom_no) - - if not flt(d.planned_qty): - frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx)) - - def raise_work_orders(self): - """It will raise work order (Draft) for all distinct FG items""" - self.validate_data() - - from erpnext.utilities.transaction_base import validate_uom_is_integer - validate_uom_is_integer(self, "stock_uom", "planned_qty") - - items = self.get_production_items() - - wo_list = [] - frappe.flags.mute_messages = True - - for key in items: - work_order = self.create_work_order(items[key]) - if work_order: - wo_list.append(work_order) - - frappe.flags.mute_messages = False - - if wo_list: - wo_list = ["""%s""" % \ - (p, p) for p in wo_list] - msgprint(_("{0} created").format(comma_and(wo_list))) - else : - msgprint(_("No Work Orders created")) - - def get_production_items(self): - item_dict = {} - for d in self.get("items"): - item_details= { - "production_item" : d.item_code, - "sales_order" : d.sales_order, - "material_request" : d.material_request, - "material_request_item" : d.material_request_item, - "bom_no" : d.bom_no, - "description" : d.description, - "stock_uom" : d.stock_uom, - "company" : self.company, - "wip_warehouse" : "", - "fg_warehouse" : d.warehouse, - "status" : "Draft", - "project" : frappe.db.get_value("Sales Order", d.sales_order, "project") - } - - """ Club similar BOM and item for processing in case of Sales Orders """ - if self.get_items_from == "Material Request": - item_details.update({ - "qty": d.planned_qty - }) - item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details - - else: - item_details.update({ - "qty":flt(item_dict.get((d.item_code, d.sales_order, d.warehouse),{}) - .get("qty")) + flt(d.planned_qty) - }) - item_dict[(d.item_code, d.sales_order, d.warehouse)] = item_details - - return item_dict - - def create_work_order(self, item_dict): - """Create work order. Called from Production Planning Tool""" - from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse - warehouse = get_default_warehouse() - wo = frappe.new_doc("Work Order") - wo.update(item_dict) - wo.set_work_order_operations() - if warehouse: - wo.wip_warehouse = warehouse.get('wip_warehouse') - if not wo.fg_warehouse: - wo.fg_warehouse = warehouse.get('fg_warehouse') - - try: - wo.insert() - return wo.name - except OverProductionError: - pass - - def get_so_wise_planned_qty(self): - """ - bom_dict { - bom_no: ['sales_order', 'qty'] - } - """ - bom_dict = {} - for d in self.get("items"): - if self.get_items_from == "Material Request": - bom_dict.setdefault(d.bom_no, []).append([d.material_request_item, flt(d.planned_qty)]) - else: - bom_dict.setdefault(d.bom_no, []).append([d.sales_order, flt(d.planned_qty)]) - return bom_dict - - def download_raw_materials(self): - """ Create csv data for required raw material to produce finished goods""" - self.validate_data() - bom_dict = self.get_so_wise_planned_qty() - self.get_raw_materials(bom_dict) - return self.get_csv() - - def get_raw_materials(self, bom_dict,non_stock_item=0): - """ Get raw materials considering sub-assembly items - { - "item_code": [qty_required, description, stock_uom, min_order_qty] - } - """ - item_list = [] - precision = frappe.get_precision("BOM Item", "stock_qty") - - for bom, so_wise_qty in bom_dict.items(): - bom_wise_item_details = {} - if self.use_multi_level_bom and self.only_raw_materials and self.include_subcontracted: - # get all raw materials with sub assembly childs - # Did not use qty_consumed_per_unit in the query, as it leads to rounding loss - for d in frappe.db.sql("""select fb.item_code, - ifnull(sum(fb.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, - fb.description, fb.stock_uom, item.min_order_qty - from `tabBOM Explosion Item` fb, `tabBOM` bom, `tabItem` item - where bom.name = fb.parent and item.name = fb.item_code - and (item.is_sub_contracted_item = 0 or ifnull(item.default_bom, "")="") - """ + ("and item.is_stock_item = 1","")[non_stock_item] + """ - and fb.docstatus<2 and bom.name=%(bom)s - group by fb.item_code, fb.stock_uom""", {"bom":bom}, as_dict=1): - bom_wise_item_details.setdefault(d.item_code, d) - else: - # Get all raw materials considering SA items as raw materials, - # so no childs of SA items - bom_wise_item_details = self.get_subitems(bom_wise_item_details, bom,1, \ - self.use_multi_level_bom,self.only_raw_materials, self.include_subcontracted,non_stock_item) - - for item, item_details in bom_wise_item_details.items(): - for so_qty in so_wise_qty: - item_list.append([item, flt(flt(item_details.qty) * so_qty[1], precision), - item_details.description, item_details.stock_uom, item_details.min_order_qty, - so_qty[0]]) - - self.make_items_dict(item_list) - - def get_subitems(self,bom_wise_item_details, bom, parent_qty, include_sublevel, only_raw, supply_subs,non_stock_item=0): - items = frappe.db.sql(""" - SELECT - bom_item.item_code, - default_material_request_type, - ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, - item.is_sub_contracted_item as is_sub_contracted, - item.default_bom as default_bom, - bom_item.description as description, - bom_item.stock_uom as stock_uom, - item.min_order_qty as min_order_qty - FROM - `tabBOM Item` bom_item, - `tabBOM` bom, - tabItem item - where - bom.name = bom_item.parent - and bom.name = %(bom)s - and bom_item.docstatus < 2 - and bom_item.item_code = item.name - """ + ("and item.is_stock_item = 1", "")[non_stock_item] + """ - group by bom_item.item_code""", {"bom": bom, "parent_qty": parent_qty}, as_dict=1) - - for d in items: - if ((d.default_material_request_type == "Purchase" - and not (d.is_sub_contracted and only_raw and include_sublevel)) - or (d.default_material_request_type == "Manufacture" and not only_raw)): - - if d.item_code in bom_wise_item_details: - bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty - else: - bom_wise_item_details[d.item_code] = d - - if include_sublevel and d.default_bom: - if ((d.default_material_request_type == "Purchase" and d.is_sub_contracted and supply_subs) - or (d.default_material_request_type == "Manufacture")): - - my_qty = 0 - projected_qty = self.get_item_projected_qty(d.item_code) - if self.create_material_requests_for_all_required_qty: - my_qty = d.qty - else: - total_required_qty = flt(bom_wise_item_details.get(d.item_code, frappe._dict()).qty) - if (total_required_qty - d.qty) < projected_qty: - my_qty = total_required_qty - projected_qty - else: - my_qty = d.qty - - if my_qty > 0: - self.get_subitems(bom_wise_item_details, - d.default_bom, my_qty, include_sublevel, only_raw, supply_subs) - - return bom_wise_item_details - - def make_items_dict(self, item_list): - if not getattr(self, "item_dict", None): - self.item_dict = {} - - for i in item_list: - self.item_dict.setdefault(i[0], []).append([flt(i[1]), i[2], i[3], i[4], i[5]]) - - def get_csv(self): - item_list = [['Item Code', 'Description', 'Stock UOM', 'Required Qty', 'Warehouse', - 'Quantity Requested for Purchase', 'Ordered Qty', 'Actual Qty']] - for item in self.item_dict: - total_qty = sum([flt(d[0]) for d in self.item_dict[item]]) - item_list.append([item, self.item_dict[item][0][1], self.item_dict[item][0][2], total_qty]) - item_qty = frappe.db.sql("""select warehouse, indented_qty, ordered_qty, actual_qty - from `tabBin` where item_code = %s""", item, as_dict=1) - - i_qty, o_qty, a_qty = 0, 0, 0 - for w in item_qty: - i_qty, o_qty, a_qty = i_qty + flt(w.indented_qty), o_qty + \ - flt(w.ordered_qty), a_qty + flt(w.actual_qty) - - item_list.append(['', '', '', '', w.warehouse, flt(w.indented_qty), - flt(w.ordered_qty), flt(w.actual_qty)]) - if item_qty: - item_list.append(['', '', '', '', 'Total', i_qty, o_qty, a_qty]) - else: - item_list.append(['', '', '', '', 'Total', 0, 0, 0]) - - return item_list - - def raise_material_requests(self): - """ - Raise Material Request if projected qty is less than qty required - Requested qty should be shortage qty considering minimum order qty - """ - self.validate_data() - if not self.purchase_request_for_warehouse: - frappe.throw(_("Please enter Warehouse for which Material Request will be raised")) - - bom_dict = self.get_so_wise_planned_qty() - self.get_raw_materials(bom_dict,self.create_material_requests_non_stock_request) - - if self.item_dict: - self.create_material_request() - - def get_requested_items(self): - items_to_be_requested = frappe._dict() - - if not self.create_material_requests_for_all_required_qty: - item_projected_qty = self.get_projected_qty() - - for item, so_item_qty in self.item_dict.items(): - total_qty = sum([flt(d[0]) for d in so_item_qty]) - requested_qty = 0 - - if self.create_material_requests_for_all_required_qty: - requested_qty = total_qty - elif total_qty > item_projected_qty.get(item, 0): - # shortage - requested_qty = total_qty - flt(item_projected_qty.get(item)) - # consider minimum order qty - - if requested_qty and requested_qty < flt(so_item_qty[0][3]): - requested_qty = flt(so_item_qty[0][3]) - - # distribute requested qty SO wise - for item_details in so_item_qty: - if requested_qty: - sales_order = item_details[4] or "No Sales Order" - if self.get_items_from == "Material Request": - sales_order = "No Sales Order" - if requested_qty <= item_details[0]: - adjusted_qty = requested_qty - else: - adjusted_qty = item_details[0] - - items_to_be_requested.setdefault(item, {}).setdefault(sales_order, 0) - items_to_be_requested[item][sales_order] += adjusted_qty - requested_qty -= adjusted_qty - else: - break - - # requested qty >= total so qty, due to minimum order qty - if requested_qty: - items_to_be_requested.setdefault(item, {}).setdefault("No Sales Order", 0) - items_to_be_requested[item]["No Sales Order"] += requested_qty - - return items_to_be_requested - - def get_item_projected_qty(self,item): - conditions = "" - if self.purchase_request_for_warehouse: - conditions = " and warehouse='{0}'".format(frappe.db.escape(self.purchase_request_for_warehouse)) - - item_projected_qty = frappe.db.sql(""" - select ifnull(sum(projected_qty),0) as qty - from `tabBin` - where item_code = %(item_code)s {conditions} - """.format(conditions=conditions), { "item_code": item }, as_dict=1) - - return item_projected_qty[0].qty - - def get_projected_qty(self): - items = self.item_dict.keys() - item_projected_qty = frappe.db.sql("""select item_code, sum(projected_qty) - from `tabBin` where item_code in (%s) and warehouse=%s group by item_code""" % - (", ".join(["%s"]*len(items)), '%s'), tuple(items + [self.purchase_request_for_warehouse])) - - return dict(item_projected_qty) - - def create_material_request(self): - items_to_be_requested = self.get_requested_items() - - material_request_list = [] - if items_to_be_requested: - for item in items_to_be_requested: - item_wrapper = frappe.get_doc("Item", item) - material_request = frappe.new_doc("Material Request") - material_request.update({ - "transaction_date": nowdate(), - "status": "Draft", - "company": self.company, - "requested_by": frappe.session.user, - "schedule_date": add_days(nowdate(), cint(item_wrapper.lead_time_days)), - }) - material_request.update({"material_request_type": item_wrapper.default_material_request_type}) - - for sales_order, requested_qty in items_to_be_requested[item].items(): - material_request.append("items", { - "doctype": "Material Request Item", - "__islocal": 1, - "item_code": item, - "item_name": item_wrapper.item_name, - "description": item_wrapper.description, - "uom": item_wrapper.stock_uom, - "item_group": item_wrapper.item_group, - "brand": item_wrapper.brand, - "qty": requested_qty, - "schedule_date": add_days(nowdate(), cint(item_wrapper.lead_time_days)), - "warehouse": self.purchase_request_for_warehouse, - "sales_order": sales_order if sales_order!="No Sales Order" else None, - "project": frappe.db.get_value("Sales Order", sales_order, "project") \ - if sales_order!="No Sales Order" else None - }) - - material_request.flags.ignore_permissions = 1 - material_request.submit() - material_request_list.append(material_request.name) - - if material_request_list: - message = ["""%s""" % \ - (p, p) for p in material_request_list] - msgprint(_("Material Requests {0} created").format(comma_and(message))) - else: - msgprint(_("Nothing to request")) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index ab2f6ee8e3..b1bfeffa54 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -344,8 +344,6 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( label: __('Include Exploded Items')}, {fieldtype:'Check', fieldname:'ignore_existing_ordered_qty', label: __('Ignore Existing Ordered Qty')}, - {fieldtype:'Check', fieldname:'include_raw_materials_from_sales_order', - label: __('Include raw materials from sales order')}, { fieldtype:'Table', fieldname: 'items', description: __('Select BOM, Qty and For Warehouse'), diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 1460bc8b69..445e02b691 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -934,7 +934,7 @@ def make_raw_material_request(items, company, sales_order, project=None): item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty') item["include_raw_materials_from_sales_order"] = items.get('include_raw_materials_from_sales_order') - raw_materials = get_items_for_material_requests(items, company) + raw_materials = get_items_for_material_requests(items, sales_order, company) if not raw_materials: frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available.")) return From 158e7dcd8bcb3e80dfb60541dbbf54800a372b2c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 4 Jan 2019 11:04:37 +0530 Subject: [PATCH 57/76] fix(bom-uom_issue)Fetch rate in BOM from Price List based on UOM --- erpnext/manufacturing/doctype/bom/bom.js | 4 +- erpnext/manufacturing/doctype/bom/bom.py | 58 +++++++++++----- .../doctype/bom_item/bom_item.json | 67 ++++++++++++++++++- erpnext/public/js/controllers/transaction.js | 1 + erpnext/regional/report/gstr_1/gstr_1.py | 4 +- erpnext/stock/get_item_details.py | 11 +-- 6 files changed, 121 insertions(+), 24 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 2615b31782..7b5339b4f0 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -121,9 +121,11 @@ frappe.ui.form.on("BOM", { freeze: true, args: { update_parent: true, - from_child_bom:false + from_child_bom:false, + save: false }, callback: function(r) { + refresh_field("items"); if(!r.exc) frm.refresh_fields(); } }); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 54ffa06912..57a1cc2593 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -8,6 +8,7 @@ from frappe import _ from erpnext.setup.utils import get_exchange_rate from frappe.website.website_generator import WebsiteGenerator from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.get_item_details import get_price_list_rate import functools @@ -109,7 +110,11 @@ class BOM(WebsiteGenerator): "item_name": item.item_name, "bom_no": item.bom_no, "stock_qty": item.stock_qty, - "include_item_in_manufacturing": item.include_item_in_manufacturing + "include_item_in_manufacturing": item.include_item_in_manufacturing, + "qty": item.qty, + "uom": item.uom, + "stock_uom": item.stock_uom, + "conversion_factor": item.conversion_factor }) for r in ret: if not item.get(r): @@ -141,7 +146,7 @@ class BOM(WebsiteGenerator): 'uom' : item and args['stock_uom'] or '', 'conversion_factor': 1, 'bom_no' : args['bom_no'], - 'rate' : rate / self.conversion_rate if self.conversion_rate else rate, + 'rate' : rate, 'qty' : args.get("qty") or args.get("stock_qty") or 1, 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1, 'base_rate' : rate, @@ -173,35 +178,56 @@ class BOM(WebsiteGenerator): elif self.rm_cost_as_per == "Price List": if not self.buying_price_list: frappe.throw(_("Please select Price List")) - rate = frappe.db.get_value("Item Price", {"price_list": self.buying_price_list, - "item_code": arg["item_code"]}, "price_list_rate") or 0.0 - - price_list_currency = frappe.db.get_value("Price List", - self.buying_price_list, "currency") - if price_list_currency != self.company_currency(): - rate = flt(rate * self.conversion_rate) + args = frappe._dict({ + "doctype": "BOM", + "price_list": self.buying_price_list, + "qty": arg.get("qty"), + "uom": arg.get("uom") or arg.get("stock_uom"), + "stock_uom": arg.get("stock_uom"), + "transaction_type": "buying", + "company": self.company, + "currency": self.currency, + "conversion_rate": self.conversion_rate or 1, + "conversion_factor": arg.get("conversion_factor") or 1, + "plc_conversion_rate": 1 + }) + item_doc = frappe.get_doc("Item", arg.get("item_code")) + out = frappe._dict() + get_price_list_rate(args, item_doc, out) + rate = out.price_list_rate if not rate: - frappe.msgprint(_("{0} not found for Item {1}") - .format(self.rm_cost_as_per, arg["item_code"]), alert=True) + if self.rm_cost_as_per == "Price List": + frappe.msgprint(_("Price not found for item {0} and price list {1}") + .format(arg["item_code"], self.buying_price_list), alert=True) + else: + frappe.msgprint(_("{0} not found for item {1}") + .format(self.rm_cost_as_per, arg["item_code"]), alert=True) return flt(rate) - def update_cost(self, update_parent=True, from_child_bom=False): + def update_cost(self, update_parent=True, from_child_bom=False, save=True): if self.docstatus == 2: return existing_bom_cost = self.total_cost for d in self.get("items"): - rate = self.get_rm_rate({"item_code": d.item_code, "bom_no": d.bom_no}) - if rate: - d.rate = rate * flt(d.conversion_factor) / flt(self.conversion_rate) + d.rate = self.get_rm_rate({ + "item_code": d.item_code, + "bom_no": d.bom_no, + "qty": d.qty, + "uom": d.uom, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor + }) + d.amount = flt(d.rate) * flt(d.qty) if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True self.calculate_cost() - self.save() + if save: + self.save() self.update_exploded_items() # update parent BOMs diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index 8d4d69b09a..754540eb6b 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -1023,6 +1023,71 @@ "set_only_once": 0, "translatable": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "operation", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Item operation", + "length": 0, + "no_copy": 0, + "options": "Operation", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "allow_alternative_item", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Allow Alternative Item", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 } ], "has_web_view": 0, @@ -1035,7 +1100,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-12-28 16:38:56.529079", + "modified": "2018-11-23 15:05:55.187136", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 9dd9bebb4d..4ef8b2e8be 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -441,6 +441,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ weight_per_unit: item.weight_per_unit, weight_uom: item.weight_uom, uom : item.uom, + stock_uom: item.stock_uom, pos_profile: me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '', cost_center: item.cost_center } diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index fa2d2afce6..0c7d066216 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -120,7 +120,7 @@ class Gstr1Report(object): and customer in ('{0}')""".format("', '".join([frappe.db.escape(c.name) for c in customers])) if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): - b2c_limit = frappe.db.get_single_value('GSt Settings', 'b2c_limit') + b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') if not b2c_limit: frappe.throw(_("Please set B2C Limit in GST Settings.")) @@ -201,7 +201,7 @@ class Gstr1Report(object): if unidentified_gst_accounts: frappe.msgprint(_("Following accounts might be selected in GST Settings:") + "
" + "
".join(unidentified_gst_accounts), alert=True) - + # Build itemised tax for export invoices where tax table is blank for invoice, items in iteritems(self.invoice_items): if invoice not in self.items_based_on_tax_rate \ diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bbea9043a3..9db04cd0ab 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -370,6 +370,8 @@ def get_price_list_rate(args, item_doc, out): meta = frappe.get_meta(args.parenttype or args.doctype) if meta.get_field("currency") or args.get('currency'): + pl_details = get_price_list_currency_and_exchange_rate(args) + args.update(pl_details) validate_price_list(args) if meta.get_field("currency") and args.price_list: validate_conversion_rate(args, meta) @@ -554,9 +556,10 @@ def validate_conversion_rate(args, meta): validate_conversion_rate(args.price_list_currency, args.plc_conversion_rate, meta.get_label("plc_conversion_rate"), args.company) - args.plc_conversion_rate = flt(args.plc_conversion_rate, - get_field_precision(meta.get_field("plc_conversion_rate"), - frappe._dict({"fields": args}))) + if meta.get_field("plc_conversion_rate"): + args.plc_conversion_rate = flt(args.plc_conversion_rate, + get_field_precision(meta.get_field("plc_conversion_rate"), + frappe._dict({"fields": args}))) def get_party_item_code(args, item_doc, out): if args.transaction_type=="selling" and args.customer: @@ -792,7 +795,7 @@ def get_price_list_uom_dependant(price_list): if not result: throw(_("Price List {0} is disabled or does not exist").format(price_list)) - return result.price_not_uom_dependant + return not result.price_not_uom_dependant def get_price_list_currency_and_exchange_rate(args): From 78ccbe24a3c548353f01c4a6deb3c9fe9c69ec39 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 14 Jan 2019 20:03:45 +0530 Subject: [PATCH 58/76] fix(test): BOM raw materials rate from price list as per uom --- erpnext/manufacturing/doctype/bom/test_bom.py | 5 +++-- .../doctype/production_plan/production_plan.py | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 5b8acaf81f..1129025a69 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -76,7 +76,7 @@ class TestBOM(unittest.TestCase): # update cost of all BOMs based on latest valuation rate update_cost() - + # check if new valuation rate updated in all BOMs for d in frappe.db.sql("""select rate from `tabBOM Item` where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""", as_dict=1): @@ -97,6 +97,7 @@ class TestBOM(unittest.TestCase): self.assertEqual(bom.base_total_cost, 486000) def test_bom_cost_multi_uom_multi_currency(self): + frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependant", 1) for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)): frappe.db.sql("delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s", item_code) @@ -105,7 +106,7 @@ class TestBOM(unittest.TestCase): item_price.item_code = item_code item_price.price_list_rate = rate item_price.insert() - + bom = frappe.copy_doc(test_records[2]) bom.set_rate_of_sub_assembly_item_based_on_bom = 0 bom.rm_cost_as_per = "Price List" diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 1ad6e64d41..6c84ef15ca 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -457,7 +457,7 @@ def get_material_request_items(row, sales_order, company, ignore_existing_ordere if row['purchase_uom'] != row['stock_uom']: if not row['conversion_factor']: frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}") - .format(row['purchase_uom'], row['stock_uom'], item)) + .format(row['purchase_uom'], row['stock_uom'], row.item_code)) requested_qty = requested_qty / row['conversion_factor'] if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"): @@ -546,7 +546,7 @@ def get_items_for_material_requests(doc, sales_order=None, company=None): ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or doc.get('ignore_existing_ordered_qty') planned_qty = data.get('required_qty') or data.get('planned_qty') item_details = {} - if data.get("bom"): + if data.get("bom") or data.get("bom_no"): if data.get('required_qty'): bom_no = data.get('bom') include_non_stock_items = 1 @@ -563,7 +563,7 @@ def get_items_for_material_requests(doc, sales_order=None, company=None): if data.get('include_exploded_items') and include_subcontracted_items: # fetch exploded items from BOM item_details = get_exploded_items(item_details, - company, bom_no,include_non_stock_items, planned_qty=planned_qty) + company, bom_no, include_non_stock_items, planned_qty=planned_qty) else: item_details = get_subitems(doc, data, item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, 1, planned_qty=planned_qty) @@ -591,7 +591,6 @@ def get_items_for_material_requests(doc, sales_order=None, company=None): 'conversion_factor' : conversion_factor, } ) - if not sales_order: sales_order = doc.get("sales_order") From 3dd5f55412a5ea1aee51f978366e398abe39ebbb Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 16 Jan 2019 16:20:05 +0530 Subject: [PATCH 59/76] feat(stock-reco): Fetch items in stock reco based on group warehouse --- erpnext/regional/india/utils.py | 2 +- erpnext/stock/doctype/item/test_item.py | 3 +- .../stock_reconciliation.py | 44 ++++++++++++------- .../test_stock_reconciliation.py | 12 ++--- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 78aeee7dd7..a1fba072b2 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -11,7 +11,7 @@ def validate_gstin_for_india(doc, method): if not hasattr(doc, 'gstin'): return - doc.gstin = doc.gstin.upper().strip() + doc.gstin = doc.gstin.upper().strip() if doc.gstin else "" if not doc.gstin or doc.gstin == 'NA': return diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index abdd6765bd..d02559ebcb 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -378,7 +378,7 @@ def make_item_variant(): test_records = frappe.get_test_records('Item') -def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None): +def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, opening_stock=None): if not frappe.db.exists("Item", item_code): item = frappe.new_doc("Item") item.item_code = item_code @@ -386,6 +386,7 @@ def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None) item.description = item_code item.item_group = "All Item Groups" item.is_stock_item = is_stock_item or 1 + item.opening_stock = opening_stock or 0 item.valuation_rate = valuation_rate or 0.0 item.append("item_defaults", { "default_warehouse": warehouse or '_Test Warehouse - _TC', diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 6b5bb2825f..eb60ce56e9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -271,27 +271,39 @@ class StockReconciliation(StockController): @frappe.whitelist() def get_items(warehouse, posting_date, posting_time, company): - items = frappe.db.sql('''select i.name, i.item_name from `tabItem` i, `tabBin` bin where i.name=bin.item_code - and i.disabled=0 and bin.warehouse=%s''', (warehouse), as_dict=True) + lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) + items = frappe.db.sql(""" + select i.name, i.item_name, bin.warehouse + from tabBin bin, tabItem i + where i.name=bin.item_code and i.disabled=0 + and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse) + """, (lft, rgt)) - items += frappe.db.sql('''select i.name, i.item_name from `tabItem` i, `tabItem Default` id where i.name = id.parent - and i.is_stock_item=1 and i.has_serial_no=0 and i.has_batch_no=0 and i.has_variants=0 and i.disabled=0 - and id.default_warehouse=%s and id.company=%s group by i.name''', (warehouse, company), as_dict=True) + items += frappe.db.sql(""" + select i.name, i.item_name, id.default_warehouse + from tabItem i, `tabItem Default` id + where i.name = id.parent + and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse) + and i.is_stock_item = 1 and i.has_serial_no = 0 and i.has_batch_no = 0 + and i.has_variants = 0 and i.disabled = 0 and id.company=%s + group by i.name + """, (lft, rgt, company)) res = [] - for item in items: - qty, rate = get_stock_balance(item.name, warehouse, posting_date, posting_time, + for d in set(items): + stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time, with_valuation_rate=True) - res.append({ - "item_code": item.name, - "warehouse": warehouse, - "qty": qty, - "item_name": item.item_name, - "valuation_rate": rate, - "current_qty": qty, - "current_valuation_rate": rate - }) + if frappe.db.get_value("Item", d[0], "disabled") == 0: + res.append({ + "item_code": d[0], + "warehouse": d[2], + "qty": stock_bal[0], + "item_name": d[1], + "valuation_rate": stock_bal[1], + "current_qty": stock_bal[0], + "current_valuation_rate": stock_bal[1] + }) return res diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index bc991b4e15..2dc585b8d6 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -12,7 +12,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_per from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse -from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.item.test_item import create_item class TestStockReconciliation(unittest.TestCase): def setUp(self): @@ -83,11 +83,13 @@ class TestStockReconciliation(unittest.TestCase): def test_get_items(self): create_warehouse("_Test Warehouse Group 1", {"is_group": 1}) - create_warehouse("_Test Warehouse Ledger 1", {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"}) - make_item("_Test Stock Reco Item", {"default_warehouse": "_Test Warehouse Ledger 1 - _TC", - "is_stock_item": 1, "opening_stock": 100, "valuation_rate": 100}) + create_warehouse("_Test Warehouse Ledger 1", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"}) - items = get_items("_Test Warehouse Group 1 - _TC", nowdate(), nowtime()) + create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100, + warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100) + + items = get_items("_Test Warehouse Group 1 - _TC", nowdate(), nowtime(), "_Test Company") self.assertEqual(["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100], [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]]) From 1ed819bb10be3bf89c3e2b6bc8126991937fde3e Mon Sep 17 00:00:00 2001 From: ks093 Date: Thu, 17 Jan 2019 12:09:04 +0530 Subject: [PATCH 60/76] Refresh quotation_series field to show series --- .../doctype/shopping_cart_settings/shopping_cart_settings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js index 3219d7a5cd..e1510f5335 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js @@ -5,6 +5,7 @@ $.extend(cur_frm.cscript, { onload: function() { if(cur_frm.doc.__onload && cur_frm.doc.__onload.quotation_series) { cur_frm.fields_dict.quotation_series.df.options = cur_frm.doc.__onload.quotation_series; + cur_frm.refresh_field("quotation_series"); } }, refresh: function(){ From 61c781325d1ffcb14e23f93374d0880baec7fe4d Mon Sep 17 00:00:00 2001 From: ks093 Date: Thu, 17 Jan 2019 12:16:43 +0530 Subject: [PATCH 61/76] Fix to show low stock message. In case of order being placed with stock quantity more than available, message showing stock available wasn't showing due to tuple unpacking not happening correctly. --- erpnext/shopping_cart/cart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 254d7b74af..cc3205c7d7 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -66,7 +66,7 @@ def place_order(): sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True)) for item in sales_order.get("items"): item.reserved_warehouse, is_stock_item = frappe.db.get_value("Item", - item.item_code, ["website_warehouse", "is_stock_item"]) or None, None + item.item_code, ["website_warehouse", "is_stock_item"]) if is_stock_item: item_stock = get_qty_in_stock(item.item_code, "website_warehouse") From 425dff93d7ad15d0458b0ac98b2f5f3767a7a254 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 17 Jan 2019 12:49:45 +0530 Subject: [PATCH 62/76] fix: Project update status code and design refactored --- erpnext/hooks.py | 10 +- .../daily_work_summary/daily_work_summary.py | 6 +- erpnext/projects/doctype/project/project.json | 3571 +++++++++-------- erpnext/projects/doctype/project/project.py | 180 +- .../project_update/project_update.json | 169 +- .../doctype/project_user/project_user.json | 197 +- .../emails/daily_project_summary.html | 46 + 7 files changed, 2275 insertions(+), 1904 deletions(-) create mode 100644 erpnext/templates/emails/daily_project_summary.html diff --git a/erpnext/hooks.py b/erpnext/hooks.py index bf7e32ab78..f81cb416eb 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -223,10 +223,15 @@ doc_events = { } scheduler_events = { + "all": [ + "erpnext.projects.doctype.project.project.project_status_update_reminder" + ], "hourly": [ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', "erpnext.accounts.doctype.subscription.subscription.process_all", - "erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details" + "erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details", + "erpnext.projects.doctype.project.project.hourly_reminder", + "erpnext.projects.doctype.project.project.collect_project_status" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", @@ -245,7 +250,8 @@ scheduler_events = { "erpnext.assets.doctype.asset.asset.update_maintenance_status", "erpnext.assets.doctype.asset.asset.make_post_gl_entry", "erpnext.crm.doctype.contract.contract.update_status_for_contracts", - "erpnext.projects.doctype.project.project.update_project_sales_billing" + "erpnext.projects.doctype.project.project.update_project_sales_billing", + "erpnext.projects.doctype.project.project.send_project_status_email_to_users" ], "daily_long": [ "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" diff --git a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py index a404b5a3e3..25cda4444b 100644 --- a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py +++ b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py @@ -112,6 +112,10 @@ def get_user_emails_from_group(group): if isinstance(group_doc, string_types): group_doc = frappe.get_doc('Daily Work Summary Group', group) - emails = [d.email for d in group_doc.users if frappe.db.get_value("User", d.user, "enabled")] + emails = get_users_email(group_doc) return emails + +def get_users_email(doc): + return [d.email for d in doc.users + if frappe.db.get_value("User", d.user, "enabled")] diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 30e4c8ad44..5e6c6aa067 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -1,1872 +1,1941 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:project_name", - "beta": 0, - "creation": "2013-03-07 11:55:07", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:project_name", + "beta": 0, + "creation": "2013-03-07 11:55:07", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 0, + "engine": "InnoDB", "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "project_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Project Name", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "", + "fieldname": "project_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Project Name", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Open", - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Status", - "length": 0, - "no_copy": 1, - "oldfieldname": "status", - "oldfieldtype": "Select", - "options": "Open\nCompleted\nCancelled", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 1, + "label": "Status", + "length": 0, + "no_copy": 1, + "oldfieldname": "status", + "oldfieldtype": "Select", + "options": "Open\nCompleted\nCancelled", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "project_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Project Type", - "length": 0, - "no_copy": 0, - "oldfieldname": "project_type", - "oldfieldtype": "Data", - "options": "Project Type", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "project_type", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Project Type", + "length": 0, + "no_copy": 0, + "oldfieldname": "project_type", + "oldfieldtype": "Data", + "options": "Project Type", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_active", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Is Active", - "length": 0, - "no_copy": 0, - "oldfieldname": "is_active", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_active", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Is Active", + "length": 0, + "no_copy": 0, + "oldfieldname": "is_active", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Task Completion", - "fieldname": "percent_complete_method", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "% Complete Method", - "length": 0, - "no_copy": 0, - "options": "Task Completion\nTask Progress\nTask Weight", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Task Completion", + "fieldname": "percent_complete_method", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "% Complete Method", + "length": 0, + "no_copy": 0, + "options": "Task Completion\nTask Progress\nTask Weight", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "percent_complete", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "% Completed", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "fieldname": "percent_complete", + "fieldtype": "Percent", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "% Completed", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "department", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Department", + "length": 0, + "no_copy": 0, + "options": "Department", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "priority", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Priority", - "length": 0, - "no_copy": 0, - "oldfieldname": "priority", - "oldfieldtype": "Select", - "options": "Medium\nLow\nHigh", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "priority", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 1, + "label": "Priority", + "length": 0, + "no_copy": 0, + "oldfieldname": "priority", + "oldfieldtype": "Select", + "options": "Medium\nLow\nHigh", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "expected_start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Expected Start Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "project_start_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "expected_start_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Expected Start Date", + "length": 0, + "no_copy": 0, + "oldfieldname": "project_start_date", + "oldfieldtype": "Date", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "expected_end_date", - "fieldtype": "Date", - "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": "Expected End Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "completion_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "fieldname": "expected_end_date", + "fieldtype": "Date", + "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": "Expected End Date", + "length": 0, + "no_copy": 0, + "oldfieldname": "completion_date", + "oldfieldtype": "Date", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "customer_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Details", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-user", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "customer_details", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Customer Details", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Section Break", + "options": "fa fa-user", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer", - "oldfieldtype": "Link", - "options": "Customer", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "customer", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 1, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Customer", + "length": 0, + "no_copy": 0, + "oldfieldname": "customer", + "oldfieldtype": "Link", + "options": "Customer", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_14", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_14", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_order", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Order", - "length": 0, - "no_copy": 0, - "options": "Sales Order", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sales_order", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sales Order", + "length": 0, + "no_copy": 0, + "options": "Sales Order", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "users_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Users", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "users_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Users", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Project will be accessible on the website to these users", - "fieldname": "users", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Users", - "length": 0, - "no_copy": 0, - "options": "Project 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, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Project will be accessible on the website to these users", + "fieldname": "users", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Users", + "length": 0, + "no_copy": 0, + "options": "Project 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, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sb_milestones", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Tasks", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-flag", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_milestones", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Tasks", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Section Break", + "options": "fa fa-flag", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tasks", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Tasks", - "length": 0, - "no_copy": 0, - "options": "Project Task", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "tasks", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Tasks", + "length": 0, + "no_copy": 0, + "options": "Project Task", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "copied_from", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Copied From", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "copied_from", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Copied From", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break0", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Notes", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-list", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "section_break0", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Notes", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Section Break", + "options": "fa fa-list", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "notes", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Notes", - "length": 0, - "no_copy": 0, - "oldfieldname": "notes", - "oldfieldtype": "Text Editor", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "notes", + "fieldtype": "Text Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Notes", + "length": 0, + "no_copy": 0, + "oldfieldname": "notes", + "oldfieldtype": "Text Editor", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break_18", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Start and End Dates", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Start and End Dates", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "actual_start_date", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Actual Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "actual_start_date", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Actual Start Date", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "actual_time", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Actual Time (in Hours)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "actual_time", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Actual Time (in Hours)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_20", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_20", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "actual_end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Actual End Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "act_completion_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "actual_end_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Actual End Date", + "length": 0, + "no_copy": 0, + "oldfieldname": "act_completion_date", + "oldfieldtype": "Date", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "project_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Costing and Billing", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-money", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "project_details", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Costing and Billing", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Section Break", + "options": "fa fa-money", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "estimated_costing", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Estimated Cost", - "length": 0, - "no_copy": 0, - "oldfieldname": "project_value", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "estimated_costing", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Estimated Cost", + "length": 0, + "no_copy": 0, + "oldfieldname": "project_value", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "total_costing_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Costing Amount (via Timesheets)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "", + "fieldname": "total_costing_amount", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Total Costing Amount (via Timesheets)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "total_expense_claim", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Expense Claim (via Expense Claims)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "", + "fieldname": "total_expense_claim", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Total Expense Claim (via Expense Claims)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_purchase_cost", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Purchase Cost (via Purchase Invoice)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "total_purchase_cost", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Total Purchase Cost (via Purchase Invoice)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Company", + "length": 0, + "no_copy": 0, + "options": "Company", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_28", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_28", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_sales_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Sales Amount (via Sales Order)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "total_sales_amount", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Total Sales Amount (via Sales Order)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "total_billable_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Billable Amount (via Timesheets)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "", + "fieldname": "total_billable_amount", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Total Billable Amount (via Timesheets)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_billed_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Billed Amount (via Sales Invoices)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "total_billed_amount", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Total Billed Amount (via Sales Invoices)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_consumed_material_cost", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Consumed Material Cost (via Stock Entry)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "total_consumed_material_cost", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Total Consumed Material Cost (via Stock Entry)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cost_center", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Cost Center", - "length": 0, - "no_copy": 0, - "options": "Cost Center", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cost_center", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Default Cost Center", + "length": 0, + "no_copy": 0, + "options": "Cost Center", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "margin", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Margin", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "margin", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Margin", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Column Break", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0, "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gross_margin", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gross Margin", - "length": 0, - "no_copy": 0, - "oldfieldname": "gross_margin_value", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "gross_margin", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Gross Margin", + "length": 0, + "no_copy": 0, + "oldfieldname": "gross_margin_value", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_37", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_37", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "per_gross_margin", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gross Margin %", - "length": 0, - "no_copy": 0, - "oldfieldname": "per_gross_margin", - "oldfieldtype": "Currency", - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "per_gross_margin", + "fieldtype": "Percent", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Gross Margin %", + "length": 0, + "no_copy": 0, + "oldfieldname": "per_gross_margin", + "oldfieldtype": "Currency", + "options": "", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "monitor_progress", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Monitor Progress", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "monitor_progress", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Monitor Progress", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "collect_progress", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Collect Progress", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "collect_progress", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Collect Progress", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.collect_progress == true", - "fieldname": "frequency", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Frequency To Collect Progress", - "length": 0, - "no_copy": 0, - "options": "Hourly\nTwice Daily\nDaily\nWeekly", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "collect_progress", + "fieldname": "holiday_list", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Holiday List", + "length": 0, + "no_copy": 0, + "options": "Holiday List", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_45", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.collect_progress == true", + "fieldname": "frequency", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Frequency To Collect Progress", + "length": 0, + "no_copy": 0, + "options": "Hourly\nTwice Daily\nDaily\nWeekly", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.frequency == \"Hourly\" && doc.collect_progress == true)", - "fieldname": "from", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "From", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:(doc.frequency == \"Hourly\" && doc.collect_progress)", + "fieldname": "from_time", + "fieldtype": "Time", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "From Time", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.frequency == \"Hourly\" && doc.collect_progress == true)", - "fieldname": "to", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "To", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:(doc.frequency == \"Hourly\" && doc.collect_progress)", + "fieldname": "to_time", + "fieldtype": "Time", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "To Time", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.frequency == \"Twice Daily\" && doc.collect_progress == true)\n\n", - "fieldname": "first_email", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "First Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:(doc.frequency == \"Twice Daily\" && doc.collect_progress == true)\n\n", + "fieldname": "first_email", + "fieldtype": "Time", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "First Email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.frequency == \"Twice Daily\" && doc.collect_progress == true)", - "fieldname": "second_email", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Second Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:(doc.frequency == \"Twice Daily\" && doc.collect_progress == true)", + "fieldname": "second_email", + "fieldtype": "Time", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Second Email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.frequency == \"Daily\" && doc.collect_progress == true)", - "fieldname": "daily_time_to_send", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Time to send", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:(doc.frequency == \"Daily\" && doc.collect_progress == true)", + "fieldname": "daily_time_to_send", + "fieldtype": "Time", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Time to send", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", - "fieldname": "day_to_send", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Day to Send", - "length": 0, - "no_copy": 0, - "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", + "fieldname": "day_to_send", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Day to Send", + "length": 0, + "no_copy": 0, + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", - "fieldname": "weekly_time_to_send", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Time to send", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", + "fieldname": "weekly_time_to_send", + "fieldtype": "Time", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Time to send", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_45", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "collect_progress", + "description": "Message will sent to users to get their status on the project", + "fieldname": "message", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-puzzle-piece", - "idx": 29, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 4, - "modified": "2018-08-30 00:12:09.649654", - "modified_by": "Administrator", - "module": "Projects", - "name": "Project", - "owner": "Administrator", + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-puzzle-piece", + "idx": 29, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 4, + "modified": "2019-01-16 23:26:57.376682", + "modified_by": "Administrator", + "module": "Projects", + "name": "Project", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Projects User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Projects User", + "set_user_permissions": 0, + "share": 1, + "submit": 0, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 1, - "print": 0, - "read": 1, - "report": 1, - "role": "All", - "set_user_permissions": 0, - "share": 0, - "submit": 0, + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 1, + "print": 0, + "read": 1, + "report": 1, + "role": "All", + "set_user_permissions": 0, + "share": 0, + "submit": 0, "write": 0 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Projects Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Projects Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "customer, status, priority, is_active", - "show_name_in_global_search": 1, - "sort_order": "DESC", - "timeline_field": "customer", - "track_changes": 0, - "track_seen": 1, + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "search_fields": "customer, status, priority, is_active", + "show_name_in_global_search": 1, + "sort_order": "DESC", + "timeline_field": "customer", + "track_changes": 0, + "track_seen": 1, "track_views": 0 } \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index de45ec30a6..a988dc03fa 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -3,16 +3,17 @@ from __future__ import unicode_literals import frappe - -from frappe.utils import flt, getdate, get_url, now +import datetime from frappe import _ - -from frappe.model.document import Document +from six import iteritems +from email_reply_parser import EmailReplyParser +from frappe.utils import (flt, getdate, get_url, now, + nowtime, get_time, today, get_datetime, add_days) from erpnext.controllers.queries import get_filters_cond from frappe.desk.reportview import get_match_cond -import datetime - -from six import iteritems +from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email +from erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group import is_holiday_today +from frappe.model.document import Document class Project(Document): def get_feed(self): @@ -406,56 +407,137 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters): def get_cost_center_name(project): return frappe.db.get_value("Project", project, "cost_center") -@frappe.whitelist() def hourly_reminder(): - project = frappe.db.sql("""SELECT `tabProject`.name FROM `tabProject` WHERE `tabProject`.frequency = "Hourly" and (CURTIME() BETWEEN `tabProject`.from and `tabProject`.to) AND `tabProject`.collect_progress = 1 ORDER BY `tabProject`.name;""") - create_project_update(project) + fields = ["from_time", "to_time"] + projects = get_projects_for_collect_progress("Hourly", fields) -@frappe.whitelist() -def twice_daily_reminder(): - project = frappe.db.sql("""SELECT `tabProject User`.user FROM `tabProject User` INNER JOIN `tabProject` ON `tabProject`.project_name = `tabProject User`.parent WHERE (`tabProject`.frequency = "Twice Daily") AND ((`tabProject`.first_email BETWEEN DATE_ADD(curtime(), INTERVAL -15 MINUTE) AND DATE_ADD(curtime(), INTERVAL 15 MINUTE)) OR (`tabProject`.second_email BETWEEN DATE_ADD(curtime(), INTERVAL -15 MINUTE) AND DATE_ADD(curtime(), INTERVAL 15 MINUTE))) AND `tabProject`.collect_progress = 1;""") - create_project_update(project) + for project in projects: + if (get_time(nowtime()) >= get_time(project.from_time) or + get_time(nowtime()) <= get_time(project.to_time)): + send_project_update_email_to_users(project.name) + +def project_status_update_reminder(): + daily_reminder() + twice_daily_reminder() + weekly_reminder() -@frappe.whitelist() def daily_reminder(): - project = frappe.db.sql("""SELECT `tabProject User`.user FROM `tabProject User` INNER JOIN `tabProject` ON `tabProject`.project_name = `tabProject User`.parent WHERE (`tabProject`.frequency = "Daily") AND (`tabProject`.daily_time_to_send BETWEEN DATE_ADD(curtime(), INTERVAL -15 MINUTE) AND DATE_ADD(curtime(), INTERVAL 15 MINUTE)) AND `tabProject`.collect_progress = 1;""") - create_project_update(project) + fields = ["daily_time_to_send"] + projects = get_projects_for_collect_progress("Daily", fields) -@frappe.whitelist() -def weekly(): - today = datetime.datetime.now().strftime("%A") - project = frappe.db.sql("""SELECT `tabProject User`.user FROM `tabProject User` INNER JOIN `tabProject` ON `tabProject`.project_name = `tabProject User`.parent WHERE (`tabProject`.frequency = "Weekly") AND (`tabProject`.day_to_send = %s) AND (`tabProject`.weekly_time_to_send BETWEEN DATE_ADD(curtime(), INTERVAL -15 MINUTE) AND DATE_ADD(curtime(), INTERVAL 15 MINUTE)) AND `tabProject`.collect_progress = 1""", today) - create_project_update(project) + for project in projects: + if not check_project_update_exists(project.name, project.get("daily_time_to_send")): + send_project_update_email_to_users(project.name) -#Call this function in order to generate the Project Update for a specific project -def create_project_update(project): - data = [] - date_today = datetime.date.today() - time_now = frappe.utils.now_datetime().strftime('%H:%M:%S') - for projects in project: - project_update_dict = { - "doctype" : "Project Update", - "project" : projects[0], - "date": date_today, - "time": time_now, - "naming_series": "UPDATE-.project.-.YY.MM.DD.-" +def twice_daily_reminder(): + fields = ["first_email", "second_email"] + projects = get_projects_for_collect_progress("Twice Daily", fields) + + for project in projects: + for d in fields: + if not check_project_update_exists(project.name, project.get(d)): + send_project_update_email_to_users(project.name) + +def weekly_reminder(): + fields = ["day_to_send", "weekly_time_to_send"] + projects = get_projects_for_collect_progress("Weekly", fields) + + current_day = get_datetime().strftime("%A") + for project in projects: + if current_day != project.day_to_send: + continue + + if not check_project_update_exists(project.name, project.get("weekly_time_to_send")): + send_project_update_email_to_users(project.name) + +def check_project_update_exists(project, time): + data = frappe.db.sql(""" SELECT name from `tabProject Update` + WHERE project = %s and date = %s and time >= %s """, (project, today(), time)) + + return True if data and data[0][0] else False + +def get_projects_for_collect_progress(frequency, fields): + fields.extend(["name"]) + + return frappe.get_all("Project", fields = fields, + filters = {'collect_progress': 1, 'frequency': frequency}) + +def send_project_update_email_to_users(project): + doc = frappe.get_doc('Project', project) + + if is_holiday_today(doc.holiday_list) or not doc.users: return + + project_update = frappe.get_doc({ + "doctype" : "Project Update", + "project" : project, + "sent": 0, + "date": today(), + "time": nowtime(), + "naming_series": "UPDATE-.project.-.YY.MM.DD.-", + }).insert() + + subject = "Project %s update your project status" % (project) + + incoming_email_account = frappe.db.get_value('Email Account', + dict(enable_incoming=1, default_incoming=1), 'email_id') + + frappe.sendmail(recipients=get_users_email(doc), + message=doc.message, + subject=_(subject), + reference_doctype=project_update.doctype, + reference_name=project_update.name, + reply_to=incoming_email_account + ) + +def collect_project_status(): + for data in frappe.get_all("Project Update", + {'date': today(), 'sent': 0}): + replies = frappe.get_all('Communication', + fields=['content', 'text_content', 'sender'], + filters=dict(reference_doctype="Project Update", + reference_name=data.name, + communication_type='Communication', + sent_or_received='Received'), + order_by='creation asc') + + for d in replies: + doc = frappe.get_doc("Project Update", data.name) + user_data = frappe.db.get_values("User", {"email": d.sender}, + ["full_name", "user_image", "name"], as_dict=True)[0] + + doc.append("users", { + 'user': user_data.name, + 'full_name': user_data.full_name, + 'image': user_data.user_image, + 'project_status': frappe.utils.md_to_html( + EmailReplyParser.parse_reply(d.text_content) or d.content + ) + }) + + doc.save(ignore_permissions=True) + +def send_project_status_email_to_users(): + yesterday = today() + + for d in frappe.get_all("Project Update", + {'date': yesterday, 'sent': 0}): + doc = frappe.get_doc("Project Update", d.name) + + project_doc = frappe.get_doc('Project', doc.project) + + args = { + "users": doc.users, + "title": _("Project Summary for {0}").format(yesterday) } - project_update = frappe.get_doc(project_update_dict) - project_update.insert() - #you can edit your local_host - local_host = "http://localhost:8003" - project_update_url = "" % (local_host +"/desk#Form/Project%20Update/" + (project_update.name)) + ("CREATE PROJECT UPDATE" + "") - data.append(project_update_url) - email = frappe.db.sql("""SELECT user from `tabProject User` WHERE parent = %s;""", project[0]) - for emails in email: - frappe.sendmail( - recipients=emails, - subject=frappe._(projects[0]), - header=[frappe._("Please Update your Project Status"), 'blue'], - message= project_update_url - ) - return data + frappe.sendmail(recipients=get_users_email(project_doc), + template='daily_project_summary', + args=args, + subject=_("Daily Project Summary for {0}").format(d.name), + reference_doctype="Project Update", + reference_name=d.name) + + doc.db_set('sent', 1) def update_project_sales_billing(): sales_update_frequency = frappe.db.get_single_value("Selling Settings", "sales_update_frequency") diff --git a/erpnext/projects/doctype/project_update/project_update.json b/erpnext/projects/doctype/project_update/project_update.json index 8bcf397efe..497b2b7328 100644 --- a/erpnext/projects/doctype/project_update/project_update.json +++ b/erpnext/projects/doctype/project_update/project_update.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, @@ -13,6 +14,40 @@ "editable_grid": 1, "engine": "InnoDB", "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "", + "fieldname": "naming_series", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Series", + "length": 0, + "no_copy": 0, + "options": "PROJ-UPD-.YYYY.-", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -46,6 +81,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "sent", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sent", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -172,39 +240,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "progress", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "How is the Project Progressing Right Now?", - "length": 0, - "no_copy": 0, - "options": "Not Updated\nGreat/Quickly\nGood/Steady\nChallenging/Slow\nProblematic/Stuck", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -238,38 +273,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "progress_details", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Progress Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -301,40 +304,6 @@ "set_only_once": 0, "translatable": 0, "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Series", - "length": 0, - "no_copy": 0, - "options": "PROJ-UPD-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 } ], "has_web_view": 0, @@ -347,7 +316,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 14:44:21.287709", + "modified": "2019-01-16 19:31:05.210656", "modified_by": "Administrator", "module": "Projects", "name": "Project Update", diff --git a/erpnext/projects/doctype/project_user/project_user.json b/erpnext/projects/doctype/project_user/project_user.json index 596681676f..458028ff51 100644 --- a/erpnext/projects/doctype/project_user/project_user.json +++ b/erpnext/projects/doctype/project_user/project_user.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, @@ -44,6 +45,136 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_from": "user.email", + "fieldname": "email", + "fieldtype": "Read Only", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_from": "user.user_image", + "fieldname": "image", + "fieldtype": "Read Only", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 1, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "user.full_name", + "fieldname": "full_name", + "fieldtype": "Read Only", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Full Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -107,6 +238,70 @@ "set_only_once": 0, "translatable": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:parent.doctype == 'Project Update'", + "fieldname": "project_status", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Project Status", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 } ], "has_web_view": 0, @@ -119,7 +314,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-09-09 12:39:38.376816", + "modified": "2019-01-17 17:10:05.339735", "modified_by": "Administrator", "module": "Projects", "name": "Project User", diff --git a/erpnext/templates/emails/daily_project_summary.html b/erpnext/templates/emails/daily_project_summary.html new file mode 100644 index 0000000000..8b60830db6 --- /dev/null +++ b/erpnext/templates/emails/daily_project_summary.html @@ -0,0 +1,46 @@ + + +

{{ title }}

+ +
+{% for user in users %} + + + + + + + + + + +
+ {% if user.image %} + + {% else %} +
+ {{ user.full_name[0] }} +
+ {% endif %} +
+
+ {{ user.full_name }} +
+
+ + + + + + + + +
+
+ {{ user.project_status }} +
+
+ + +
+{% endfor %} \ No newline at end of file From 5a87ac0ad839c1553e8e8dd267cc98cff715ffd8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 17 Jan 2019 18:34:09 +0530 Subject: [PATCH 63/76] fix: Inter company stock transfer for serialised items --- erpnext/stock/doctype/serial_no/serial_no.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 1568a7ae4d..491ddeb89d 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -302,6 +302,9 @@ def has_duplicate_serial_no(sn, sle): if sn.warehouse: return True + if sn.company != sle.company: + return False + status = False if sn.purchase_document_no: if sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and \ @@ -357,6 +360,7 @@ def auto_make_serial_nos(args): sr.warehouse = args.get('warehouse') if args.get('actual_qty', 0) > 0 else None sr.batch_no = args.get('batch_no') sr.location = args.get('location') + sr.company = args.get('company') if sr.sales_order and args.get('voucher_type') == "Stock Entry" \ and not args.get('actual_qty', 0) > 0: sr.sales_order = None From 30667c160deb1d675affe47b17c1fba2b1dad503 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 17 Jan 2019 19:19:17 +0530 Subject: [PATCH 64/76] test: Inter company stock transfer for serialised items --- erpnext/stock/doctype/serial_no/serial_no.py | 16 ++++++------- .../stock/doctype/serial_no/test_serial_no.py | 24 +++++++++++++++++++ .../stock/doctype/warehouse/test_warehouse.py | 16 +++++++++---- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 491ddeb89d..8a797c3ebc 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -139,7 +139,7 @@ class SerialNo(StockController): order by posting_date desc, posting_time desc, name desc""", ("%%%s%%" % self.name, self.item_code), as_dict=1): if self.name.upper() in get_serial_nos(sle.serial_no): - if cint(sle.actual_qty) > 0: + if sle.actual_qty > 0: sle_dict.setdefault("incoming", []).append(sle) else: sle_dict.setdefault("outgoing", []).append(sle) @@ -218,7 +218,7 @@ def validate_serial_no(sle, item_det): frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code), SerialNoItemError) - if cint(sle.actual_qty) > 0 and has_duplicate_serial_no(sr, sle): + if sle.actual_qty > 0 and has_duplicate_serial_no(sr, sle): frappe.throw(_("Serial No {0} has already been received").format(serial_no), SerialNoDuplicateError) @@ -228,7 +228,7 @@ def validate_serial_no(sle, item_det): if return_against and return_against != sr.delivery_document_no: frappe.throw(_("Serial no {0} has been already returned").format(sr.name)) - if cint(sle.actual_qty) < 0: + if sle.actual_qty < 0: if sr.warehouse!=sle.warehouse: frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse), SerialNoWarehouseError) @@ -279,16 +279,16 @@ def validate_serial_no(sle, item_det): "parent": sales_invoice, "item_code": sle.item_code}, "sales_order") if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) - elif cint(sle.actual_qty) < 0: + elif sle.actual_qty < 0: # transfer out frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError) - elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series: + elif sle.actual_qty < 0 or not item_det.serial_no_series: frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError) elif serial_nos: for serial_no in serial_nos: sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1) - if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse: + if sr and sle.actual_qty < 0 and sr.warehouse != sle.warehouse: frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}") .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse)) @@ -323,7 +323,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): in Manufacture / Repack type Stock Entry """ allow_serial_nos = False - if sle.voucher_type=="Stock Entry" and cint(sle.actual_qty) > 0: + if sle.voucher_type=="Stock Entry" and sle.actual_qty > 0: stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no) if stock_entry.purpose in ("Repack", "Manufacture"): for d in stock_entry.get("items"): @@ -335,7 +335,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): return allow_serial_nos def update_serial_nos(sle, item_det): - if sle.is_cancelled == "No" and not sle.serial_no and cint(sle.actual_qty) > 0 \ + if sle.is_cancelled == "No" and not sle.serial_no and sle.actual_qty > 0 \ and item_det.has_serial_no == 1 and item_det.serial_no_series: serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty) frappe.db.set(sle, "serial_no", serial_nos) diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 89062f9ec8..ed70790b2c 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -7,6 +7,12 @@ from __future__ import unicode_literals import frappe, unittest +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + test_dependencies = ["Item"] test_records = frappe.get_test_records('Serial No') @@ -29,3 +35,21 @@ class TestSerialNo(unittest.TestCase): sr.warehouse = "_Test Warehouse - _TC" self.assertTrue(SerialNoCannotCannotChangeError, sr.save) + + def test_inter_company_transfer(self): + se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") + serial_nos = get_serial_nos(se.get("items")[0].serial_no) + + create_delivery_note(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0]) + + wh = create_warehouse("_Test Warehouse", company="_Test Company 1") + make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0], + company="_Test Company 1", warehouse=wh) + + serial_no = frappe.db.get_value("Serial No", serial_nos[0], ["warehouse", "company"], as_dict=1) + + self.assertEqual(serial_no.warehouse, wh) + self.assertEqual(serial_no.company, "_Test Company 1") + + def tearDown(self): + frappe.db.rollback() \ No newline at end of file diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index b033f86d8e..dc39e101ce 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -8,6 +8,7 @@ from erpnext import set_perpetual_inventory from frappe.test_runner import make_test_records from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account +import erpnext import frappe import unittest test_records = frappe.get_test_records('Warehouse') @@ -90,17 +91,24 @@ class TestWarehouse(unittest.TestCase): self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": "Test Warehouse for Merging 2 - _TC"})) -def create_warehouse(warehouse_name, properties=None): - if not frappe.db.exists("Warehouse", warehouse_name + " - _TC"): +def create_warehouse(warehouse_name, properties=None, company=None): + if not company: + company = "_Test Company" + + warehouse_id = erpnext.encode_company_abbr(warehouse_name, company) + if not frappe.db.exists("Warehouse", warehouse_id): w = frappe.new_doc("Warehouse") w.warehouse_name = warehouse_name w.parent_warehouse = "_Test Warehouse Group - _TC" - w.company = "_Test Company" + w.company = company make_account_for_warehouse(warehouse_name, w) - w.account = warehouse_name + " - _TC" + w.account = warehouse_id if properties: w.update(properties) w.save() + return w.name + else: + return warehouse_id def make_account_for_warehouse(warehouse_name, warehouse_obj): if not frappe.db.exists("Account", warehouse_name + " - _TC"): From 6202d0ed687a282571461cbf19e0d5e7d26b068b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 18 Jan 2019 09:40:24 +0530 Subject: [PATCH 65/76] fix: NoneType comparison with int error --- erpnext/stock/doctype/serial_no/serial_no.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 8a797c3ebc..491ddeb89d 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -139,7 +139,7 @@ class SerialNo(StockController): order by posting_date desc, posting_time desc, name desc""", ("%%%s%%" % self.name, self.item_code), as_dict=1): if self.name.upper() in get_serial_nos(sle.serial_no): - if sle.actual_qty > 0: + if cint(sle.actual_qty) > 0: sle_dict.setdefault("incoming", []).append(sle) else: sle_dict.setdefault("outgoing", []).append(sle) @@ -218,7 +218,7 @@ def validate_serial_no(sle, item_det): frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code), SerialNoItemError) - if sle.actual_qty > 0 and has_duplicate_serial_no(sr, sle): + if cint(sle.actual_qty) > 0 and has_duplicate_serial_no(sr, sle): frappe.throw(_("Serial No {0} has already been received").format(serial_no), SerialNoDuplicateError) @@ -228,7 +228,7 @@ def validate_serial_no(sle, item_det): if return_against and return_against != sr.delivery_document_no: frappe.throw(_("Serial no {0} has been already returned").format(sr.name)) - if sle.actual_qty < 0: + if cint(sle.actual_qty) < 0: if sr.warehouse!=sle.warehouse: frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse), SerialNoWarehouseError) @@ -279,16 +279,16 @@ def validate_serial_no(sle, item_det): "parent": sales_invoice, "item_code": sle.item_code}, "sales_order") if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) - elif sle.actual_qty < 0: + elif cint(sle.actual_qty) < 0: # transfer out frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError) - elif sle.actual_qty < 0 or not item_det.serial_no_series: + elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series: frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError) elif serial_nos: for serial_no in serial_nos: sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1) - if sr and sle.actual_qty < 0 and sr.warehouse != sle.warehouse: + if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse: frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}") .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse)) @@ -323,7 +323,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): in Manufacture / Repack type Stock Entry """ allow_serial_nos = False - if sle.voucher_type=="Stock Entry" and sle.actual_qty > 0: + if sle.voucher_type=="Stock Entry" and cint(sle.actual_qty) > 0: stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no) if stock_entry.purpose in ("Repack", "Manufacture"): for d in stock_entry.get("items"): @@ -335,7 +335,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): return allow_serial_nos def update_serial_nos(sle, item_det): - if sle.is_cancelled == "No" and not sle.serial_no and sle.actual_qty > 0 \ + if sle.is_cancelled == "No" and not sle.serial_no and cint(sle.actual_qty) > 0 \ and item_det.has_serial_no == 1 and item_det.serial_no_series: serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty) frappe.db.set(sle, "serial_no", serial_nos) From 7e71e132a3640eb23f8562e24af83a00c9e7b6a6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 17 Jan 2019 19:26:25 +0530 Subject: [PATCH 66/76] Added patch --- erpnext/patches.txt | 3 ++- .../v11_0/renamed_from_to_fields_in_project.py | 13 +++++++++++++ erpnext/projects/doctype/project/project.py | 7 +++---- 3 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 erpnext/patches/v11_0/renamed_from_to_fields_in_project.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7bf63a2290..a703fd9790 100755 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -580,4 +580,5 @@ erpnext.patches.v11_0.update_delivery_trip_status erpnext.patches.v10_0.repost_gle_for_purchase_receipts_with_rejected_items erpnext.patches.v11_0.set_missing_gst_hsn_code erpnext.patches.v11_0.rename_bom_wo_fields -erpnext.patches.v11_0.rename_additional_salary_component_additional_salary \ No newline at end of file +erpnext.patches.v11_0.rename_additional_salary_component_additional_salary +erpnext.patches.v11_0.renamed_from_to_fields_in_project \ No newline at end of file diff --git a/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py b/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py new file mode 100644 index 0000000000..4f68440002 --- /dev/null +++ b/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py @@ -0,0 +1,13 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + frappe.reload_doc('projects', 'doctype', 'project') + + if frappe.db.has_column('Project', 'from'): + rename_field('Project', 'from', 'from_time') + rename_field('Project', 'to', 'to_time') \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index a988dc03fa..9654ca345e 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import frappe -import datetime from frappe import _ from six import iteritems from email_reply_parser import EmailReplyParser @@ -476,7 +475,7 @@ def send_project_update_email_to_users(project): "naming_series": "UPDATE-.project.-.YY.MM.DD.-", }).insert() - subject = "Project %s update your project status" % (project) + subject = "For project %s, update your status" % (project) incoming_email_account = frappe.db.get_value('Email Account', dict(enable_incoming=1, default_incoming=1), 'email_id') @@ -490,7 +489,7 @@ def send_project_update_email_to_users(project): ) def collect_project_status(): - for data in frappe.get_all("Project Update", + for data in frappe.get_all("Project Update", {'date': today(), 'sent': 0}): replies = frappe.get_all('Communication', fields=['content', 'text_content', 'sender'], @@ -517,7 +516,7 @@ def collect_project_status(): doc.save(ignore_permissions=True) def send_project_status_email_to_users(): - yesterday = today() + yesterday = add_days(today(), -1) for d in frappe.get_all("Project Update", {'date': yesterday, 'sent': 0}): From 442c85c006922d8d095c8186d3fdfcc0c2f615ac Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Fri, 18 Jan 2019 21:28:37 +0530 Subject: [PATCH 67/76] fix(marketplace): Capitalize marketplace factory class to support router changes (#16428) --- erpnext/public/js/hub/hub_factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/hub/hub_factory.js b/erpnext/public/js/hub/hub_factory.js index 7d9fefc8b9..2efc826110 100644 --- a/erpnext/public/js/hub/hub_factory.js +++ b/erpnext/public/js/hub/hub_factory.js @@ -1,6 +1,6 @@ frappe.provide('erpnext.hub'); -frappe.views.marketplaceFactory = class marketplaceFactory extends frappe.views.Factory { +frappe.views.MarketplaceFactory = class MarketplaceFactory extends frappe.views.Factory { show() { is_marketplace_disabled() .then(disabled => { From 79d4400c160f98ebe639081c4a59668c6f134b7a Mon Sep 17 00:00:00 2001 From: Himanshu Date: Sat, 19 Jan 2019 14:01:38 +0530 Subject: [PATCH 68/76] fix: Offline pos fix (#16424) * Offline POS fix * Fixed items search and load items --- erpnext/accounts/page/pos/pos.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 04c8718ad9..c89035c33f 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1135,18 +1135,15 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ }, apply_category: function() { - frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name", (r) => { - category = this.selected_item_group || r.name; - - if(category == r.name) { - return this.item_data - } else { - return this.item_data.filter(function(element, index, array){ - return element.item_group == category; - }); - } - }) - + var me = this; + category = this.selected_item_group || "All Item Groups"; + if(category == 'All Item Groups') { + return this.item_data + } else { + return this.item_data.filter(function(element, index, array){ + return element.item_group == category; + }); + } }, bind_items_event: function() { From ff1078f2710378e44bd680cabc5414869eeccd26 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Mon, 21 Jan 2019 09:43:56 +0530 Subject: [PATCH 69/76] fix(minor): fetch payment terms if voucher nos are available --- .../accounts/report/accounts_receivable/accounts_receivable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 22d4d94cd0..95cb351d2e 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -190,7 +190,7 @@ class ReceivablePayableReport(object): dn_details = get_dn_details(args.get("party_type"), voucher_nos) self.voucher_details = get_voucher_details(args.get("party_type"), voucher_nos, dn_details) - if self.filters.based_on_payment_terms: + if self.filters.based_on_payment_terms and gl_entries_data: self.payment_term_map = self.get_payment_term_detail(voucher_nos) for gle in gl_entries_data: From a39f324682e9923cd3ed851d39a4f5f4aef61f00 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 21 Jan 2019 18:50:50 +0530 Subject: [PATCH 70/76] fix: Disable rounded total field's value honours docfield's default value --- erpnext/public/js/controllers/buying.js | 20 ++++++++++--------- .../authorization_rule.json | 5 +++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index cefdbe7a32..cb9c23bb62 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -28,7 +28,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ if (this.frm.doc.__islocal && frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) { - this.frm.set_value("disable_rounded_total", cint(frappe.sys_defaults.disable_rounded_total)); + + var df = frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total"); + var disable = df.default || cint(frappe.sys_defaults.disable_rounded_total); + this.frm.set_value("disable_rounded_total", disable); } /* eslint-disable */ @@ -119,7 +122,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ if (doc.doctype == "Purchase Order" && item.blanket_order_rate) { item_rate = item.blanket_order_rate; } - item.discount_amount = flt(item_rate) * flt(item.discount_percentage) / 100; + item.discount_amount = flt(item_rate) * flt(item.discount_percentage) / 100; item.rate = flt((item.price_list_rate) - (item.discount_amount), precision('rate', item)); this.calculate_taxes_and_totals(); @@ -266,26 +269,26 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ d.qty = d.qty - my_qty; cur_frm.doc.items[i].stock_qty = my_qty*cur_frm.doc.items[i].conversion_factor; cur_frm.doc.items[i].qty = my_qty; - + frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + cur_frm.doc.items[i].idx + ")"); if (qty > 0) { frappe.msgprint("Splitting " + qty + " units of " + d.item_code); var newrow = frappe.model.add_child(cur_frm.doc, cur_frm.doc.items[i].doctype, "items"); item_length++; - + for (var key in cur_frm.doc.items[i]) { newrow[key] = cur_frm.doc.items[i][key]; } - + newrow.idx = item_length; newrow["stock_qty"] = newrow.conversion_factor*qty; newrow["qty"] = qty; - + newrow["material_request"] = ""; newrow["material_request_item"] = ""; - + } } }); @@ -302,7 +305,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ if (doc.auto_repeat) { frappe.call({ method:"frappe.desk.doctype.auto_repeat.auto_repeat.update_reference", - args:{ + args:{ docname: doc.auto_repeat, reference:doc.name }, @@ -427,4 +430,3 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { }); dialog.show(); } - diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.json b/erpnext/setup/doctype/authorization_rule/authorization_rule.json index e70bf5cb47..caca56c897 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.json +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, @@ -352,7 +353,7 @@ "fieldname": "to_emp", "fieldtype": "Link", "hidden": 0, - "ignore_user_permissions": 0, + "ignore_user_permissions": 1, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, @@ -618,7 +619,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 16:15:42.627233", + "modified": "2019-01-21 17:10:39.822125", "modified_by": "Administrator", "module": "Setup", "name": "Authorization Rule", From 11ce45077d72316c359bcc8fe8c6ab8577f01f59 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Jan 2019 18:53:05 +0530 Subject: [PATCH 71/76] [Fix] Not able to submit work order --- erpnext/manufacturing/doctype/work_order/work_order.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 7b2f9a4bff..22d74e8ee6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -349,7 +349,8 @@ frappe.ui.form.on("Work Order", { before_submit: function(frm) { frm.toggle_reqd(["fg_warehouse", "wip_warehouse"], true); frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true); - frm.toggle_reqd("transfer_material_against", frm.doc.operations); + frm.toggle_reqd("transfer_material_against", + frm.doc.operations && frm.doc.operations.length > 0); frm.fields_dict.operations.grid.toggle_reqd("workstation", frm.doc.operations); }, From de718dacb21ad3592160848cc289056e7fb0455b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Jan 2019 19:47:17 +0530 Subject: [PATCH 72/76] fix: Code cleanup --- erpnext/accounts/doctype/sales_invoice/pos.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index 7348e1f8ec..287da08ef5 100755 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -250,10 +250,12 @@ def get_serial_no_data(pos_profile, company): cond = "1=1" if pos_profile.get('update_stock') and pos_profile.get('warehouse'): - cond = "warehouse = '{0}'".format(pos_profile.get('warehouse')) + cond = "warehouse = %(warehouse)s" - serial_nos = frappe.db.sql("""select name, warehouse, item_code from `tabSerial No` where {0} - and company = %(company)s """.format(cond), {'company': company}, as_dict=1) + serial_nos = frappe.db.sql("""select name, warehouse, item_code + from `tabSerial No` where {0} and company = %(company)s """.format(cond),{ + 'company': company, 'warehouse': frappe.db.escape(pos_profile.get('warehouse')) + }, as_dict=1) itemwise_serial_no = {} for sn in serial_nos: From a334ed879f5a781a5ac985af555b4443dd5adfe1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 22 Jan 2019 10:41:50 +0530 Subject: [PATCH 73/76] Update __init__.py --- erpnext/regional/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/__init__.py b/erpnext/regional/__init__.py index 510ed58766..1bd3357a81 100644 --- a/erpnext/regional/__init__.py +++ b/erpnext/regional/__init__.py @@ -7,5 +7,5 @@ from erpnext import get_region def check_deletion_permission(doc, method): region = get_region() - if region in ["Nepal", "France"]: - frappe.throw(_("Deletion is not permitted for country {0}".format(region))) \ No newline at end of file + if region in ["Nepal", "France"] and doc.docstatus != 0: + frappe.throw(_("Deletion is not permitted for country {0}".format(region))) From d157ae17b741d4237f9f3b75646bcf06df46c650 Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Tue, 22 Jan 2019 13:26:13 +0530 Subject: [PATCH 74/76] fix(ar-summary-report): Changes to column list to match recent changes in AR report --- .../accounts_receivable_summary/accounts_receivable_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 190031abb8..842ecdb8e9 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -180,7 +180,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): def get_voucherwise_data(self, party_naming_by, args): voucherwise_data = ReceivablePayableReport(self.filters).run(args)[1] - cols = ["posting_date", "party"] + cols = ["posting_date", "party", "customer-contact"] if party_naming_by == "Naming Series": cols += ["party_name"] From 1e61dd024b13bf809d4cf5cb3ee878df8878c51c Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 22 Jan 2019 15:53:48 +0550 Subject: [PATCH 75/76] bumped to version 10.1.78 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 26755dde61..db9ca3309e 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '10.1.77' +__version__ = '10.1.78' def get_default_company(user=None): '''Get default company for user''' From 64d774d178d4205963307127dd1ec1b336d9ba94 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 22 Jan 2019 16:05:56 +0550 Subject: [PATCH 76/76] bumped to version 11.0.3-beta.35 --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index f81cb416eb..bbadd7f150 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -12,7 +12,7 @@ app_license = "GNU General Public License (v3)" source_link = "https://github.com/frappe/erpnext" develop_version = '12.x.x-develop' -staging_version = '11.0.3-beta.34' +staging_version = '11.0.3-beta.35' error_report_email = "support@erpnext.com"