diff --git a/.travis.yml b/.travis.yml index f20128d2cf..747de20ba9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,17 +33,16 @@ before_script: - cd ~/frappe-bench - bench get-app erpnext $TRAVIS_BUILD_DIR - bench use test_site - - bench reinstall --mariadb-root-username root --mariadb-root-password travis --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 --mariadb-root-username root --mariadb-root-password travis --yes + - bench scheduler disable + - sed -i 's/9000/9001/g' sites/common_site_config.json + - bench start & + - sleep 10 - set -e - bench run-tests --app erpnext --coverage after_script: diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 4197d54734..29e42440dd 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.78' def get_default_company(user=None): '''Get default company for user''' 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 diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index e6fe6caf14..810b6f79b5 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/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 = [] 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: 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() { 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: 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"] 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 diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2ed664c9bf..01211a976f 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 @@ -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 @@ -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!='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/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", diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d855873d5a..65629d2818 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_expense_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() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fe6b39dd05..cd373b84d7 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})") 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/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 63e89ab6e3..0b19b7aaf9 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,20 @@ 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.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), 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): @@ -428,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 diff --git a/erpnext/hooks.py b/erpnext/hooks.py index bf7e32ab78..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" @@ -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/hr/utils.py b/erpnext/hr/utils.py index f35eb5919e..02262012f1 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -54,6 +54,9 @@ 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: self.assign_task_to_users(task, set(users)) 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/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/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/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 24ce7d41f7..6c84ef15ca 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -7,9 +7,9 @@ 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 six import string_types, iteritems from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults class ProductionPlan(Document): @@ -372,40 +372,46 @@ 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.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}) 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, - 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 @@ -413,45 +419,61 @@ 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 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 requested_qty > 0 and requested_qty < row['min_order_qty']: + requested_qty = row['min_order_qty'] + 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'], row.item_code)) + 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, + return { + 'item_code': row.item_code, '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, - 'sales_order': data.get('sales_order') - }) + 'min_order_qty': row['min_order_qty'], + 'sales_order': sales_order + } def get_sales_orders(self): so_filter = item_filter = "" @@ -487,8 +509,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() @@ -497,56 +519,96 @@ 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: - 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` 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): +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 = {} - - 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 + 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") or data.get("bom_no"): + if data.get('required_qty'): + bom_no = data.get('bom') + include_non_stock_items = 1 + include_subcontracted_items = 1 if data.get('include_exploded_items') else 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'))) + bom_no = data.get('bom_no') + include_subcontracted_items = doc.get('include_subcontracted_items') + include_non_stock_items = doc.get('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(): - 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 not planned_qty: + frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx'))) - return doc['mr_items'] + 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() + 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[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 not sales_order: + sales_order = doc.get("sales_order") + + 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/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); }, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9b8a69d2b2..69ad5d1d74 100755 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -1,9 +1,10 @@ execute:import unidecode # new requirement erpnext.patches.v8_0.move_perpetual_inventory_setting +erpnext.patches.v8_9.set_print_zero_amount_taxes +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 @@ -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.delete_gst_doctypes_for_outside_india_accounts erpnext.patches.v8_9.set_default_fields_in_variant_settings @@ -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 @@ -580,3 +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 +erpnext.patches.v11_0.renamed_from_to_fields_in_project \ 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") 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/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.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..9654ca345e 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -3,16 +3,16 @@ from __future__ import unicode_literals import frappe - -from frappe.utils import flt, getdate, get_url, now 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 +406,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 = "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') + + 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 = add_days(today(), -1) + + 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/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 649d73a63f..371fc5c79b 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 EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass class Task(NestedSet): nsm_parent_field = 'parent_task' @@ -43,6 +44,12 @@ 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'")) + if(self.project): + if frappe.db.exists("Project", self.project): + 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": for d in self.depends_on: diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 0966b76807..6fb5412473 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -5,11 +5,11 @@ 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, EndDateCannotBeGreaterThanProjectEndDateError 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) @@ -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(), 35), add_days(nowdate(), 45), save=False) + pro = frappe.get_doc("Project", task_end.project) + pro.expected_end_date = add_days(nowdate(), 40) + pro.save() + self.assertRaises(EndDateCannotBeGreaterThanProjectEndDateError, task_end.save) + + + +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" @@ -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 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/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 035c58d6d2..4ef8b2e8be 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, @@ -440,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/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 => { 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))) 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/regional/india/utils.py b/erpnext/regional/india/utils.py index ea600e4618..a1fba072b2 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -11,22 +11,49 @@ 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-zA-Z]{5}[0-9]{4}[a-zA-Z]{1}[1-9A-Za-z]{1}[Z]{1}[0-9a-zA-Z]{1}") - if not p.match(doc.gstin): - frappe.throw(_("Invalid GSTIN or Enter NA for Unregistered")) + doc.gstin = doc.gstin.upper().strip() if doc.gstin else "" + if not doc.gstin or doc.gstin == 'NA': + return + + if len(doc.gstin) != 15: + frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters.")) + + 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.")) + + validate_gstin_check_digit(doc.gstin) if not doc.gst_state: - if doc.state in states: - doc.gst_state = doc.state + if not doc.state: + return + 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 - 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)) + 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' + mod = len(code_point_chars) + input_chars = gstin[:-1] + for char in input_chars: + digit = factor * code_point_chars.find(char) + digit = (digit // mod) + (digit % mod) + total += digit + factor = 2 if factor == 1 else 1 + 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'): 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/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 0da82724ac..2970d7a5e8 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -177,6 +177,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 cc2fc59f41..fa4f83551d 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -98,6 +98,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") diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 3b36a2dca5..78fb0c1404 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 diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 54d7654c2d..bc74142475 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') { @@ -367,7 +381,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.json b/erpnext/selling/doctype/sales_order/sales_order.json index aa29f3a264..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, @@ -4015,7 +4081,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-01-07 16:51:50.857438", + "modified": "2019-01-09 16:51:47.917329", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 229f4f6837..b589cdeaa4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -147,7 +147,7 @@ class SalesOrder(SellingController): super(SalesOrder, self).validate_with_previous_doc({ "Quotation": { "ref_dn_field": "prevdoc_docname", - "compare_fields": [["company", "="], ["currency", "="]] + "compare_fields": [["company", "="]] } }) @@ -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) + 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 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 diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index c078a08249..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 @@ -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 @@ -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, increment): + 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([from_date, period_end_date]) - from_date = period_end_date + relativedelta(days=1) + self.periodic_daterange.append(period_end_date) + + from_date = add_days(period_end_date, 1) if period_end_date == to_date: break 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 = [ 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", 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' }, diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 6bc9036195..e1e1b44889 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] @@ -147,7 +150,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/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") 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(){ 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/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 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 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/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 212bb51185..2dc585b8d6 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 create_item class TestStockReconciliation(unittest.TestCase): def setUp(self): @@ -79,6 +81,19 @@ 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"}) + + 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"]]) + 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 006945e1aa..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,19 +91,28 @@ class TestWarehouse(unittest.TestCase): self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": "Test Warehouse for Merging 2 - _TC"})) -def create_warehouse(warehouse_name): - 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"): - 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 diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 692fe5d0fb..9db04cd0ab 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'): @@ -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,15 @@ 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") + 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)) @@ -336,7 +342,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)) @@ -364,22 +370,24 @@ 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) 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: if args.price_list and args.rate: 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) @@ -548,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: @@ -786,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): 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