diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index 5e764037a74..c417a493c69 100644 --- a/erpnext/accounts/custom/address.py +++ b/erpnext/accounts/custom/address.py @@ -33,6 +33,8 @@ def get_shipping_address(company, address = None): if address and frappe.db.get_value('Dynamic Link', {'parent': address, 'link_name': company}): filters.append(["Address", "name", "=", address]) + if not address: + filters.append(["Address", "is_shipping_address", "=", 1]) address = frappe.get_all("Address", filters=filters, fields=fields) or {} diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index dd346bc2408..335e8a15ab0 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): amount, base_amount = calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency) + if not amount: + return + if via_journal_entry: book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) @@ -298,17 +301,21 @@ def process_deferred_accounting(posting_date=None): start_date = add_months(today(), -1) end_date = add_days(today(), -1) - for record_type in ('Income', 'Expense'): - doc = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=posting_date, - start_date=start_date, - end_date=end_date, - type=record_type - )) + companies = frappe.get_all('Company') - doc.insert() - doc.submit() + for company in companies: + for record_type in ('Income', 'Expense'): + doc = frappe.get_doc(dict( + doctype='Process Deferred Accounting', + company=company.name, + posting_date=posting_date, + start_date=start_date, + end_date=end_date, + type=record_type + )) + + doc.insert() + doc.submit() def make_gl_entries(doc, credit_account, debit_account, against, amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 7cd1e7736c8..fac28c92397 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -19,7 +19,7 @@ class AccountingDimension(Document): def validate(self): if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', - 'Cost Center', 'Accounting Dimension Detail', 'Company') : + 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') : msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) frappe.throw(msg) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 0ff7230e554..703e93c0757 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -260,7 +260,7 @@ "description": "If enabled, ledger entries will be posted for change amount in POS transactions", "fieldname": "post_change_gl_entries", "fieldtype": "Check", - "label": "Change Ledger Entries for Change Amount" + "label": "Create Ledger Entries for Change Amount" } ], "icon": "icon-cog", @@ -268,7 +268,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-06-16 13:14:45.739107", + "modified": "2021-06-17 20:26:03.721202", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 5f110e2727c..ffc9d1c4658 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -51,7 +51,7 @@ class BankStatementImport(DataImport): self.import_file, self.google_sheets_url ) - if 'Bank Account' not in json.dumps(preview): + if 'Bank Account' not in json.dumps(preview['columns']): frappe.throw(_("Please add the Bank Account column")) from frappe.core.page.background_jobs.background_jobs import get_info diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 54623dd6cd5..51f18a5a4e3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -690,7 +690,7 @@ "options": "Account" }, { - "depends_on": "eval:doc.received_amount", + "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", "fieldtype": "Currency", "label": "Received Amount After Tax", @@ -707,7 +707,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-09 11:55:04.215050", + "modified": "2021-06-22 20:37:06.154206", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b6b2bef9633..adaf99a7900 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -706,7 +706,7 @@ class PaymentEntry(AccountsController): if account_currency != self.company_currency: frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) - if self.payment_type == 'Pay': + if self.payment_type in ('Pay', 'Internal Transfer'): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" @@ -761,7 +761,7 @@ class PaymentEntry(AccountsController): return self.advance_tax_account elif self.payment_type == 'Receive': return self.paid_from - elif self.payment_type == 'Pay': + elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_to def update_advance_paid(self): diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index f58c8f45262..dc9094c3e91 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }); }, - company: function() { - erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); - }, - onload: function() { this._super(); @@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", { frm: frm, freeze_message: __("Creating Purchase Receipt ...") }) - } + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, }) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index a714ac78274..00ef7d5c184 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -175,9 +175,7 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "naming_series", @@ -189,9 +187,7 @@ "options": "ACC-PINV-.YYYY.-\nACC-PINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1, - "show_days": 1, - "show_seconds": 1 + "set_only_once": 1 }, { "fieldname": "supplier", @@ -203,9 +199,7 @@ "options": "Supplier", "print_hide": 1, "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "bold": 1, @@ -217,9 +211,7 @@ "label": "Supplier Name", "oldfieldname": "supplier_name", "oldfieldtype": "Data", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fetch_from": "supplier.tax_id", @@ -227,27 +219,21 @@ "fieldtype": "Read Only", "label": "Tax Id", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "due_date", "fieldtype": "Date", "label": "Due Date", "oldfieldname": "due_date", - "oldfieldtype": "Date", - "show_days": 1, - "show_seconds": 1 + "oldfieldtype": "Date" }, { "default": "0", "fieldname": "is_paid", "fieldtype": "Check", "label": "Is Paid", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", @@ -255,25 +241,19 @@ "fieldtype": "Check", "label": "Is Return (Debit Note)", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply Tax Withholding Amount", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -283,17 +263,13 @@ "label": "Company", "options": "Company", "print_hide": 1, - "remember_last_selected_value": 1, - "show_days": 1, - "show_seconds": 1 + "remember_last_selected_value": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center", - "show_days": 1, - "show_seconds": 1 + "options": "Cost Center" }, { "default": "Today", @@ -305,9 +281,7 @@ "oldfieldtype": "Date", "print_hide": 1, "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "posting_time", @@ -316,8 +290,6 @@ "no_copy": 1, "print_hide": 1, "print_width": "100px", - "show_days": 1, - "show_seconds": 1, "width": "100px" }, { @@ -326,9 +298,7 @@ "fieldname": "set_posting_time", "fieldtype": "Check", "label": "Edit Posting Date and Time", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "amended_from", @@ -340,58 +310,44 @@ "oldfieldtype": "Link", "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:doc.on_hold", "fieldname": "sb_14", "fieldtype": "Section Break", - "label": "Hold Invoice", - "show_days": 1, - "show_seconds": 1 + "label": "Hold Invoice" }, { "default": "0", "fieldname": "on_hold", "fieldtype": "Check", - "label": "Hold Invoice", - "show_days": 1, - "show_seconds": 1 + "label": "Hold Invoice" }, { "depends_on": "eval:doc.on_hold", "description": "Once set, this invoice will be on hold till the set date", "fieldname": "release_date", "fieldtype": "Date", - "label": "Release Date", - "show_days": 1, - "show_seconds": 1 + "label": "Release Date" }, { "fieldname": "cb_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "depends_on": "eval:doc.on_hold", "fieldname": "hold_comment", "fieldtype": "Small Text", - "label": "Reason For Putting On Hold", - "show_days": 1, - "show_seconds": 1 + "label": "Reason For Putting On Hold" }, { "collapsible": 1, "collapsible_depends_on": "bill_no", "fieldname": "supplier_invoice_details", "fieldtype": "Section Break", - "label": "Supplier Invoice Details", - "show_days": 1, - "show_seconds": 1 + "label": "Supplier Invoice Details" }, { "fieldname": "bill_no", @@ -399,15 +355,11 @@ "label": "Supplier Invoice No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "bill_date", @@ -416,17 +368,13 @@ "no_copy": 1, "oldfieldname": "bill_date", "oldfieldtype": "Date", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "depends_on": "return_against", "fieldname": "returns", "fieldtype": "Section Break", - "label": "Returns", - "show_days": 1, - "show_seconds": 1 + "label": "Returns" }, { "depends_on": "return_against", @@ -436,34 +384,26 @@ "no_copy": 1, "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact", - "show_days": 1, - "show_seconds": 1 + "label": "Address and Contact" }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_person", @@ -471,67 +411,51 @@ "in_global_search": 1, "label": "Contact Person", "options": "Contact", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "label": "Contact", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_email", "fieldtype": "Small Text", "label": "Contact Email", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tag" }, { "fieldname": "currency", @@ -540,9 +464,7 @@ "oldfieldname": "currency", "oldfieldtype": "Select", "options": "Currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "conversion_rate", @@ -551,24 +473,18 @@ "oldfieldname": "conversion_rate", "oldfieldtype": "Currency", "precision": "9", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break2", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "price_list_currency", @@ -576,18 +492,14 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "plc_conversion_rate", "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", @@ -596,15 +508,11 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "depends_on": "update_stock", @@ -613,9 +521,7 @@ "fieldtype": "Link", "label": "Set Accepted Warehouse", "options": "Warehouse", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "depends_on": "update_stock", @@ -625,15 +531,11 @@ "label": "Rejected Warehouse", "no_copy": 1, "options": "Warehouse", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "No", @@ -641,33 +543,25 @@ "fieldtype": "Select", "label": "Raw Materials Supplied", "options": "No\nYes", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-shopping-cart" }, { "default": "0", "fieldname": "update_stock", "fieldtype": "Check", "label": "Update Stock", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode", - "show_days": 1, - "show_seconds": 1 + "label": "Scan Barcode" }, { "allow_bulk_edit": 1, @@ -677,56 +571,43 @@ "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Purchase Invoice Item", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules", - "show_days": 1, - "show_seconds": 1 + "label": "Pricing Rules" }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible_depends_on": "supplied_items", "fieldname": "raw_materials_supplied", "fieldtype": "Section Break", - "label": "Raw Materials Supplied", - "show_days": 1, - "show_seconds": 1 + "label": "Raw Materials Supplied" }, { + "depends_on": "update_stock", "fieldname": "supplied_items", "fieldtype": "Table", "label": "Supplied Items", - "options": "Purchase Receipt Item Supplied", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "options": "Purchase Receipt Item Supplied" }, { "fieldname": "section_break_26", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_total", @@ -734,9 +615,7 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_net_total", @@ -746,24 +625,18 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_28", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "net_total", @@ -773,56 +646,42 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-money" }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_49", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", "options": "Shipping Rule", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "section_break_51", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "taxes_and_charges", @@ -831,9 +690,7 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "taxes", @@ -841,17 +698,13 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges", - "show_days": 1, - "show_seconds": 1 + "options": "Purchase Taxes and Charges" }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup", - "show_days": 1, - "show_seconds": 1 + "label": "Tax Breakup" }, { "fieldname": "other_charges_calculation", @@ -860,17 +713,13 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-money" }, { "fieldname": "base_taxes_and_charges_added", @@ -880,9 +729,7 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -892,9 +739,7 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -904,15 +749,11 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_40", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "taxes_and_charges_added", @@ -922,9 +763,7 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -934,9 +773,7 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_taxes_and_charges", @@ -944,18 +781,14 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount", - "show_days": 1, - "show_seconds": 1 + "label": "Additional Discount" }, { "default": "Grand Total", @@ -963,9 +796,7 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_discount_amount", @@ -973,38 +804,28 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_46", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "section_break_49", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "base_grand_total", @@ -1014,9 +835,7 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1026,9 +845,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1038,9 +855,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_in_words", @@ -1050,17 +865,13 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break8", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -1071,9 +882,7 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1083,9 +892,7 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1095,9 +902,7 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "in_words", @@ -1107,9 +912,7 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_advance", @@ -1120,9 +923,7 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "outstanding_amount", @@ -1133,18 +934,14 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "0", "depends_on": "grand_total", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total", - "show_days": 1, - "show_seconds": 1 + "label": "Disable Rounded Total" }, { "collapsible": 1, @@ -1152,26 +949,20 @@ "depends_on": "eval:doc.is_paid===1||(doc.advances && doc.advances.length>0)", "fieldname": "payments_section", "fieldtype": "Section Break", - "label": "Payments", - "show_days": 1, - "show_seconds": 1 + "label": "Payments" }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", "options": "Mode of Payment", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "cash_bank_account", "fieldtype": "Link", "label": "Cash/Bank Account", - "options": "Account", - "show_days": 1, - "show_seconds": 1 + "options": "Account" }, { "fieldname": "clearance_date", @@ -1179,15 +970,11 @@ "label": "Clearance Date", "no_copy": 1, "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_br_payments", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "depends_on": "is_paid", @@ -1196,9 +983,7 @@ "label": "Paid Amount", "no_copy": 1, "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_paid_amount", @@ -1207,9 +992,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, @@ -1217,9 +1000,7 @@ "depends_on": "grand_total", "fieldname": "write_off", "fieldtype": "Section Break", - "label": "Write Off", - "show_days": 1, - "show_seconds": 1 + "label": "Write Off" }, { "fieldname": "write_off_amount", @@ -1227,9 +1008,7 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_write_off_amount", @@ -1238,15 +1017,11 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_61", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1254,9 +1029,7 @@ "fieldtype": "Link", "label": "Write Off Account", "options": "Account", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1264,9 +1037,7 @@ "fieldtype": "Link", "label": "Write Off Cost Center", "options": "Cost Center", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -1276,17 +1047,13 @@ "label": "Advance Payments", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", "fieldname": "allocate_advances_automatically", "fieldtype": "Check", - "label": "Set Advances and Allocate (FIFO)", - "show_days": 1, - "show_seconds": 1 + "label": "Set Advances and Allocate (FIFO)" }, { "depends_on": "eval:!doc.allocate_advances_automatically", @@ -1294,9 +1061,7 @@ "fieldtype": "Button", "label": "Get Advances Paid", "oldfieldtype": "Button", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "advances", @@ -1306,26 +1071,20 @@ "oldfieldname": "advance_allocation_details", "oldfieldtype": "Table", "options": "Purchase Invoice Advance", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:(!doc.is_return)", "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms", - "show_days": 1, - "show_seconds": 1 + "label": "Payment Terms" }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", - "options": "Payment Terms Template", - "show_days": 1, - "show_seconds": 1 + "options": "Payment Terms Template" }, { "fieldname": "payment_schedule", @@ -1333,9 +1092,7 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -1343,33 +1100,25 @@ "fieldname": "terms_section_break", "fieldtype": "Section Break", "label": "Terms and Conditions", - "options": "fa fa-legal", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-legal" }, { "fieldname": "tc_name", "fieldtype": "Link", "label": "Terms", "options": "Terms and Conditions", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", - "label": "Terms and Conditions1", - "show_days": 1, - "show_seconds": 1 + "label": "Terms and Conditions1" }, { "collapsible": 1, "fieldname": "printing_settings", "fieldtype": "Section Break", - "label": "Printing Settings", - "show_days": 1, - "show_seconds": 1 + "label": "Printing Settings" }, { "allow_on_submit": 1, @@ -1377,9 +1126,7 @@ "fieldtype": "Link", "label": "Letter Head", "options": "Letter Head", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1387,15 +1134,11 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_112", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "allow_on_submit": 1, @@ -1407,18 +1150,14 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1, - "show_days": 1, - "show_seconds": 1 + "report_hide": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, @@ -1427,9 +1166,7 @@ "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "credit_to", @@ -1440,9 +1177,7 @@ "options": "Account", "print_hide": 1, "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "party_account_currency", @@ -1452,9 +1187,7 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "No", @@ -1464,9 +1197,7 @@ "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "against_expense_account", @@ -1476,15 +1207,11 @@ "no_copy": 1, "oldfieldname": "against_expense_account", "oldfieldtype": "Small Text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_63", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "Draft", @@ -1493,9 +1220,7 @@ "in_standard_filter": 1, "label": "Status", "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "inter_company_invoice_reference", @@ -1504,9 +1229,7 @@ "no_copy": 1, "options": "Sales Invoice", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "remarks", @@ -1515,18 +1238,14 @@ "no_copy": 1, "oldfieldname": "remarks", "oldfieldtype": "Text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", "label": "Subscription Section", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1535,9 +1254,7 @@ "fieldtype": "Date", "label": "From Date", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1546,15 +1263,11 @@ "fieldtype": "Date", "label": "To Date", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_114", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "auto_repeat", @@ -1563,32 +1276,24 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference", - "show_days": 1, - "show_seconds": 1 + "label": "Update Auto Repeat Reference" }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions ", - "show_days": 1, - "show_seconds": 1 + "label": "Accounting Dimensions " }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "0", @@ -1596,9 +1301,7 @@ "fieldname": "is_internal_supplier", "fieldtype": "Check", "label": "Is Internal Supplier", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "tax_withholding_category", @@ -1606,25 +1309,19 @@ "hidden": 1, "label": "Tax Withholding Category", "options": "Tax Withholding Category", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "billing_address", "fieldtype": "Link", "label": "Select Billing Address", - "options": "Address", - "show_days": 1, - "show_seconds": 1 + "options": "Address" }, { "fieldname": "billing_address_display", "fieldtype": "Small Text", "label": "Billing Address", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "project", @@ -1638,9 +1335,7 @@ "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", "label": "Unrealized Profit / Loss Account", - "options": "Account", - "show_days": 1, - "show_seconds": 1 + "options": "Account" }, { "depends_on": "eval:doc.is_internal_supplier", @@ -1649,9 +1344,7 @@ "fieldname": "represents_company", "fieldtype": "Link", "label": "Represents Company", - "options": "Company", - "show_days": 1, - "show_seconds": 1 + "options": "Company" }, { "depends_on": "eval:doc.update_stock && doc.is_internal_supplier", @@ -1663,8 +1356,6 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", - "show_days": 1, - "show_seconds": 1, "width": "50px" }, { @@ -1692,7 +1383,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-06-09 12:30:25.632109", + "modified": "2021-06-15 18:20:56.806195", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 0ee0bc7e11f..45d89ad1c87 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController): # because updating ordered qty in bin depends upon updated ordered qty in PO if self.update_stock == 1: self.update_stock_ledger() + self.set_consumed_qty_in_po() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit update_serial_nos_after_submit(self, "items") @@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.update_stock_ledger() self.delete_auto_created_batches() + self.set_consumed_qty_in_po() self.make_gl_entries_on_cancel() diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 503dda77282..2f5d36c8fab 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -621,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(actual_qty_0, get_qty_after_transaction()) def test_subcontracting_via_purchase_invoice(self): + from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + update_backflush_based_on('BOM') make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) @@ -964,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase): update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate()) # Create Purchase Order with TDS applied - po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000) + po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item') po.apply_tds = 1 po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.save() @@ -1000,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase): # Create Purchase Invoice against Purchase Order purchase_invoice = get_mapped_purchase_invoice(po.name) purchase_invoice.allocate_advances_automatically = 1 + purchase_invoice.items[0].item_code = '_Test Non Stock Item' purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC' purchase_invoice.save() purchase_invoice.submit() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e14f305fc55..55a5b99907b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -989,7 +989,7 @@ class SalesInvoice(SellingController): for payment_mode in self.payments: if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: - payment_mode.base_amount -= self.change_amount + payment_mode.base_amount -= flt(self.change_amount) if payment_mode.amount: # POS, make payment entries diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.js b/erpnext/accounts/doctype/tax_rule/tax_rule.js index 370890e4d8e..bc497163e8b 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.js +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.js @@ -1,24 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.add_fetch("customer", "customer_group", "customer_group" ); -cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" ); - -frappe.ui.form.on("Tax Rule", "tax_type", function(frm) { - frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales"); - frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase"); -}) - -frappe.ui.form.on("Tax Rule", "onload", function(frm) { - if(frm.doc.__islocal) { - frm.set_value("use_for_shopping_cart", 1); - } -}) - -frappe.ui.form.on("Tax Rule", "refresh", function(frm) { - frappe.ui.form.trigger("Tax Rule", "tax_type"); -}) - frappe.ui.form.on("Tax Rule", "customer", function(frm) { if(frm.doc.customer) { frappe.call({ diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.json b/erpnext/accounts/doctype/tax_rule/tax_rule.json index ef155381c0b..27467484329 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.json +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.json @@ -1,1103 +1,250 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "ACC-TAX-RULE-.YYYY.-.#####", - "beta": 0, - "creation": "2015-08-07 02:33:52.670866", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "autoname": "ACC-TAX-RULE-.YYYY.-.#####", + "creation": "2015-08-07 02:33:52.670866", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "tax_type", + "use_for_shopping_cart", + "column_break_1", + "sales_tax_template", + "purchase_tax_template", + "filters", + "customer", + "supplier", + "item", + "billing_city", + "billing_county", + "billing_state", + "billing_zipcode", + "billing_country", + "tax_category", + "column_break_2", + "customer_group", + "supplier_group", + "item_group", + "shipping_city", + "shipping_county", + "shipping_state", + "shipping_zipcode", + "shipping_country", + "section_break_4", + "from_date", + "column_break_7", + "to_date", + "section_break_6", + "priority", + "column_break_20", + "company" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Sales", - "fieldname": "tax_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Tax Type", - "length": 0, - "no_copy": 0, - "options": "Sales\nPurchase", - "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 - }, + "default": "Sales", + "fieldname": "tax_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Tax Type", + "options": "Sales\nPurchase" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "use_for_shopping_cart", - "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": "Use for Shopping Cart", - "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 - }, + "default": "1", + "fieldname": "use_for_shopping_cart", + "fieldtype": "Check", + "label": "Use for Shopping Cart" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Sales\"", - "fieldname": "sales_tax_template", - "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 Tax Template", - "length": 0, - "no_copy": 0, - "options": "Sales Taxes and Charges Template", - "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 - }, + "depends_on": "eval:doc.tax_type==\"Sales\"", + "fieldname": "sales_tax_template", + "fieldtype": "Link", + "label": "Sales Tax Template", + "options": "Sales Taxes and Charges Template" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Purchase\"", - "fieldname": "purchase_tax_template", - "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": "Purchase Tax Template", - "length": 0, - "no_copy": 0, - "options": "Purchase Taxes and Charges Template", - "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 - }, + "depends_on": "eval:doc.tax_type==\"Purchase\"", + "fieldname": "purchase_tax_template", + "fieldtype": "Link", + "label": "Purchase Tax Template", + "options": "Purchase Taxes and Charges Template" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "filters", - "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": "Filters", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "filters", + "fieldtype": "Section Break", + "label": "Filters" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Sales\"", - "fieldname": "customer", - "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": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "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 - }, + "depends_on": "eval:doc.tax_type==\"Sales\"", + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Purchase\"", - "fieldname": "supplier", - "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": "Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "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 - }, + "depends_on": "eval:doc.tax_type==\"Purchase\"", + "fieldname": "supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item", - "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", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item", + "fieldtype": "Link", + "label": "Item", + "options": "Item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_city", - "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": "Billing City", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_city", + "fieldtype": "Data", + "label": "Billing City" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_county", - "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": "Billing County", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_county", + "fieldtype": "Data", + "label": "Billing County" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_state", - "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": "Billing State", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_state", + "fieldtype": "Data", + "label": "Billing State" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_zipcode", - "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": "Billing Zipcode", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_zipcode", + "fieldtype": "Data", + "label": "Billing Zipcode" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_country", - "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": "Billing Country", - "length": 0, - "no_copy": 0, - "options": "Country", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_country", + "fieldtype": "Link", + "label": "Billing Country", + "options": "Country" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tax_category", - "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": "Tax Category", - "length": 0, - "no_copy": 0, - "options": "Tax Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category" + }, { - "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 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Sales\"", - "fieldname": "customer_group", - "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": "Customer Group", - "length": 0, - "no_copy": 0, - "options": "Customer Group", - "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 - }, + "depends_on": "eval:doc.tax_type==\"Sales\"", + "fetch_from": "customer.customer_group", + "fieldname": "customer_group", + "fieldtype": "Link", + "label": "Customer Group", + "options": "Customer Group" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Purchase\"", - "fieldname": "supplier_group", - "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": "Supplier Group", - "length": 0, - "no_copy": 0, - "options": "Supplier Group", - "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 - }, + "depends_on": "eval:doc.tax_type==\"Purchase\"", + "fetch_from": "supplier.supplier_group", + "fieldname": "supplier_group", + "fieldtype": "Link", + "label": "Supplier Group", + "options": "Supplier Group" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_group", - "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 Group", - "length": 0, - "no_copy": 0, - "options": "Item Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_city", - "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": "Shipping City", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_city", + "fieldtype": "Data", + "label": "Shipping City" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_county", - "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": "Shipping County", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_county", + "fieldtype": "Data", + "label": "Shipping County" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_state", - "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": "Shipping State", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_state", + "fieldtype": "Data", + "label": "Shipping State" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_zipcode", - "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": "Shipping Zipcode", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_zipcode", + "fieldtype": "Data", + "label": "Shipping Zipcode" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_country", - "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": "Shipping Country", - "length": 0, - "no_copy": 0, - "options": "Country", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_country", + "fieldtype": "Link", + "label": "Shipping Country", + "options": "Country" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "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": "Validity", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Validity" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "From Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "To Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "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 - }, + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fieldname": "priority", - "fieldtype": "Int", - "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": "Priority", - "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 - }, + "default": "1", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority" + }, { - "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 - }, + "fieldname": "column_break_20", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-12-27 01:22:17.721636", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Tax Rule", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2021-06-04 23:14:27.186879", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Rule", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py index ac1ffd9e750..cf7226822ed 100644 --- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py @@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase): tax_rule1 = make_tax_rule(customer_group= "All Customer Groups", sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01") tax_rule1.save() - self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}), + self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}), "_Test Sales Taxes and Charges Template - _TC") def test_conflict_with_overlapping_dates(self): diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 59009ae6215..25d2cf10bd4 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None): def check_if_in_list(gle, gl_map, dimensions=None): account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type', - 'cost_center', 'project'] + 'cost_center', 'project', 'voucher_detail_no'] if dimensions: account_head_fieldnames = account_head_fieldnames + dimensions diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e025fc69054..b97dc401e6a 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where party_type = %s and party=%s + and is_cancelled = 0 group by company""", (party_type, party))) for d in companies: diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 03808c3640c..744ada9e558 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -222,7 +222,7 @@ def get_gl_entries(filters, accounting_dimensions): def get_conditions(filters): conditions = [] - if filters.get("account") and not filters.get("include_dimensions"): + if filters.get("account"): filters.account = get_accounts_with_children(filters.account) conditions.append("account in %(account)s") diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 60e675f2f1f..48bd7308bca 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -168,21 +168,24 @@ def get_columns(filters): "label": _("Income"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 305 + }, { "fieldname": "expense", "label": _("Expense"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 305 + }, { "fieldname": "gross_profit_loss", "label": _("Gross Profit / Loss"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 307 + } ] diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 630a1dc8cd5..b9c77d59b18 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -9,13 +9,14 @@ "supp_master_name", "supplier_group", "buying_price_list", + "maintain_same_rate_action", + "role_to_override_stop_action", "column_break_3", "po_required", "pr_required", "maintain_same_rate", - "maintain_same_rate_action", - "role_to_override_stop_action", "allow_multiple_items", + "bill_for_rejected_quantity_in_purchase_invoice", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -108,6 +109,13 @@ "fieldtype": "Link", "label": "Role Allowed to Override Stop Action", "options": "Role" + }, + { + "default": "1", + "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.", + "fieldname": "bill_for_rejected_quantity_in_purchase_invoice", + "fieldtype": "Check", + "label": "Bill for Rejected Quantity in Purchase Invoice" } ], "icon": "fa fa-cog", @@ -115,7 +123,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-04 20:01:44.087066", + "modified": "2021-06-24 10:38:28.934525", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 0f6d927b362..233a9c87e59 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", { } else { frm.set_value("tax_withholding_category", frm.supplier_tds); } + }, + + refresh: function(frm) { + frm.trigger('get_materials_from_supplier'); + }, + + get_materials_from_supplier: function(frm) { + let po_details = []; + + if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) { + frm.doc.supplied_items.forEach(d => { + if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { + po_details.push(d.name) + } + }); + } + + if (po_details && po_details.length) { + frm.add_custom_button(__('Return of Components'), () => { + frm.call({ + method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier', + freeze: true, + freeze_message: __('Creating Stock Entry'), + args: { purchase_order: frm.doc.name, po_details: po_details }, + callback: function(r) { + if (r && r.message) { + const doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + } + }); + }, __('Create')); + } } }); @@ -217,7 +250,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( }, has_unsupplied_items: function() { - return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty) + return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty) }, make_stock_entry: function() { @@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( ], primary_action: function() { var data = d.get_values(); + let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold; + frappe.call({ method: "frappe.desk.form.utils.add_comment", args: { reference_doctype: me.frm.doctype, reference_name: me.frm.docname, - content: __('Reason for hold:') + " " +data.reason_for_hold, + content: __(reason_for_hold), comment_email: frappe.session.user, comment_by: frappe.session.user_fullname }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 41668c62919..bb0ad60cabc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -609,6 +609,7 @@ "fieldname": "supplied_items", "fieldtype": "Table", "label": "Supplied Items", + "no_copy": 1, "oldfieldname": "po_raw_material_details", "oldfieldtype": "Table", "options": "Purchase Order Item Supplied", @@ -1377,7 +1378,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-04-19 00:55:30.781375", + "modified": "2021-05-30 15:17:53.663648", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 2629ba7d61e..eaa502ff7f0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -14,12 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status from erpnext.stock.utils import get_bin from erpnext.accounts.party import get_party_account_currency -from six import string_types from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details -from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\ - unlink_inter_company_doc +from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party, + update_linked_doc, unlink_inter_company_doc) form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -503,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions @frappe.whitelist() def make_rm_stock_entry(purchase_order, rm_items): - if isinstance(rm_items, string_types): + rm_items_list = rm_items + + if isinstance(rm_items, str): rm_items_list = json.loads(rm_items) - else: + elif not rm_items: frappe.throw(_("No Items available for transfer")) if rm_items_list: @@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items): 'qty': rm_item_data["qty"], 'from_warehouse': rm_item_data["warehouse"], 'stock_uom': rm_item_data["stock_uom"], + 'serial_no': rm_item_data.get('serial_no'), + 'batch_no': rm_item_data.get('batch_no'), 'main_item_code': rm_item_data["item_code"], 'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item') } @@ -582,3 +585,58 @@ def update_status(status, name): def make_inter_company_sales_order(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction return make_inter_company_transaction("Purchase Order", source_name, target_doc) + +@frappe.whitelist() +def get_materials_from_supplier(purchase_order, po_details): + if isinstance(po_details, str): + po_details = json.loads(po_details) + + doc = frappe.get_cached_doc('Purchase Order', purchase_order) + doc.initialized_fields() + doc.purchase_orders = [doc.name] + doc.get_available_materials() + + if not doc.available_materials: + frappe.throw(_('Materials are already received against the purchase order {0}') + .format(purchase_order)) + + return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details) + +def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details): + ste_doc = frappe.new_doc('Stock Entry') + ste_doc.purpose = 'Material Transfer' + ste_doc.purchase_order = po_doc.name + ste_doc.company = po_doc.company + ste_doc.is_return = 1 + + for key, value in available_materials.items(): + if not value.qty: + continue + + if value.batch_no: + for batch_no, qty in value.batch_no.items(): + if qty > 0: + add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no) + else: + add_items_in_ste(ste_doc, value, value.qty, po_details) + + ste_doc.set_stock_entry_type() + ste_doc.calculate_rate_and_amount() + + return ste_doc + +def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None): + item = ste_doc.append('items', row.item_details) + + po_detail = list(set(row.po_details).intersection(po_details)) + item.update({ + 'qty': qty, + 'batch_no': batch_no, + 'basic_rate': row.item_details['rate'], + 'po_detail': po_detail[0] if po_detail else '', + 's_warehouse': row.item_details['t_warehouse'], + 't_warehouse': row.item_details['s_warehouse'], + 'item_code': row.item_details['rm_item_code'], + 'subcontracted_item': row.item_details['main_item_code'], + 'serial_no': '\n'.join(row.serial_no) if row.serial_no else '' + }) \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 3b9f8e9775a..8563b97ab74 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.stock.doctype.batch.test_batch import make_new_batch -from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) def test_exploded_items_in_subcontracted(self): - item_code = "_Test Subcontracted FG Item 1" + item_code = "_Test Subcontracted FG Item 11" make_subcontracted_item(item_code=item_code) po = create_purchase_order(item_code=item_code, qty=1, @@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase): for item in rm_items: transferred_rm_map[item.get('rm_item_code')] = item - for item in pr.get('supplied_items'): - self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty) - - update_backflush_based_on("BOM") - - def test_backflushed_based_on_for_multiple_batches(self): - item_code = "_Test Subcontracted FG Item 2" - make_item('Sub Contracted Raw Material 2', { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1 - }) - - make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1, - raw_materials=["Sub Contracted Raw Material 2"]) - - update_backflush_based_on("Material Transferred for Subcontract") - - order_qty = 500 - po = create_purchase_order(item_code=item_code, qty=order_qty, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") - - make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100) - - rm_items = [ - {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item", - "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}] - - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.submit() - - for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]: - make_new_batch(batch_id=batch, item_code=item_code) - - pr = make_purchase_receipt(po.name) - - # partial receipt - pr.get('items')[0].qty = 30 - pr.get('items')[0].batch_no = "ABCD1" - - purchase_order = po.name - purchase_order_item = po.items[0].name - - for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items(): - pr.append("items", { - "item_code": pr.get('items')[0].item_code, - "item_name": pr.get('items')[0].item_name, - "uom": pr.get('items')[0].uom, - "stock_uom": pr.get('items')[0].stock_uom, - "warehouse": pr.get('items')[0].warehouse, - "conversion_factor": pr.get('items')[0].conversion_factor, - "cost_center": pr.get('items')[0].cost_center, - "rate": pr.get('items')[0].rate, - "qty": qty, - "batch_no": batch_no, - "purchase_order": purchase_order, - "purchase_order_item": purchase_order_item - }) - - pr.submit() - - pr1 = make_purchase_receipt(po.name) - pr1.get('items')[0].qty = 300 - pr1.get('items')[0].batch_no = "ABCD1" - pr1.save() - - pr_key = ("Sub Contracted Raw Material 2", po.name) - consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key) - - self.assertTrue(pr1.supplied_items[0].consumed_qty > 0) - self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty)) - update_backflush_based_on("BOM") def test_supplied_qty_against_subcontracted_po(self): @@ -1117,22 +1043,29 @@ def create_purchase_order(**args): po.conversion_factor = args.conversion_factor or 1 po.supplier_warehouse = args.supplier_warehouse or None - po.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 10, - "rate": args.rate or 500, - "schedule_date": add_days(nowdate(), 1), - "include_exploded_items": args.get('include_exploded_items', 1), - "against_blanket_order": args.against_blanket_order - }) + if args.rm_items: + for row in args.rm_items: + po.append("items", row) + else: + po.append("items", { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 10, + "rate": args.rate or 500, + "schedule_date": add_days(nowdate(), 1), + "include_exploded_items": args.get('include_exploded_items', 1), + "against_blanket_order": args.against_blanket_order + }) + + po.set_missing_values() if not args.do_not_save: po.insert() if not args.do_not_submit: if po.is_subcontracted == "Yes": supp_items = po.get("supplied_items") for d in supp_items: - d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" + if not d.reserve_warehouse: + d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" po.submit() return po diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 1dbd7c60c3e..132dd1769c5 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -97,6 +97,9 @@ "is_fixed_asset", "item_tax_rate", "section_break_72", + "production_plan", + "production_plan_item", + "production_plan_sub_assembly_item", "page_break" ], "fields": [ @@ -803,13 +806,37 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "production_plan", + "fieldtype": "Link", + "label": "Production Plan", + "options": "Production Plan", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "production_plan_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Production Plan Item", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "production_plan_sub_assembly_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Production Plan Sub Assembly Item", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-03-22 11:46:12.357435", + "modified": "2021-06-28 19:22:22.715365", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index d7ea9c1ccc8..60247bd90bb 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -6,21 +6,25 @@ "engine": "InnoDB", "field_order": [ "main_item_code", - "bom_detail_no", + "rm_item_code", + "column_break_3", "stock_uom", + "reserve_warehouse", "conversion_factor", "column_break_6", - "rm_item_code", + "bom_detail_no", "reference_name", - "reserve_warehouse", "section_break2", "rate", "col_break2", "amount", "section_break1", "required_qty", + "supplied_qty", "col_break1", - "supplied_qty" + "consumed_qty", + "returned_qty", + "total_supplied_qty" ], "fields": [ { @@ -125,6 +129,8 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Supplied Qty", + "no_copy": 1, + "print_hide": 1, "read_only": 1 }, { @@ -142,13 +148,42 @@ { "fieldname": "col_break2", "fieldtype": "Column Break" + }, + { + "fieldname": "consumed_qty", + "fieldtype": "Float", + "label": "Consumed Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "total_supplied_qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Total Supplied Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-06-09 15:17:58.128242", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index dc00bca5cc5..f9cd72015a6 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -6,10 +6,11 @@ "engine": "InnoDB", "field_order": [ "main_item_code", - "description", + "rm_item_code", + "item_name", "bom_detail_no", "col_break1", - "rm_item_code", + "description", "stock_uom", "conversion_factor", "reference_name", @@ -25,7 +26,8 @@ "secbreak_3", "batch_no", "col_break4", - "serial_no" + "serial_no", + "purchase_order" ], "fields": [ { @@ -52,7 +54,6 @@ "fieldname": "description", "fieldtype": "Text Editor", "in_global_search": 1, - "in_list_view": 1, "label": "Description", "oldfieldname": "description", "oldfieldtype": "Data", @@ -81,18 +82,20 @@ "fieldname": "required_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Required Qty", + "label": "Available Qty For Consumption", "oldfieldname": "required_qty", "oldfieldtype": "Currency", + "print_hide": 1, "read_only": 1 }, { + "columns": 2, "fieldname": "consumed_qty", "fieldtype": "Float", - "label": "Consumed Qty", + "in_list_view": 1, + "label": "Qty to Be Consumed", "oldfieldname": "consumed_qty", "oldfieldtype": "Currency", - "read_only": 1, "reqd": 1 }, { @@ -183,12 +186,28 @@ { "fieldname": "col_break4", "fieldtype": "Column Break" + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "hidden": 1, + "label": "Purchase Order", + "no_copy": 1, + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-06-19 19:33:04.431213", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Receipt Item Supplied", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 0127eb81630..a4ce84e1cf9 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items): create_rfq_items(sq_doc, supplier, data) def create_rfq_items(sq_doc, supplier, data): - sq_doc.append('items', { - "item_code": data.item_code, - "item_name": data.item_name, - "description": data.description, - "qty": data.qty, - "rate": data.rate, - "conversion_factor": data.conversion_factor if data.conversion_factor else None, - "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"), - "warehouse": data.warehouse or '', + args = {} + + for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor', + 'warehouse', 'material_request', 'material_request_item', 'stock_qty']: + args[field] = data.get(field) + + args.update({ "request_for_quotation_item": data.name, - "request_for_quotation": data.parent + "request_for_quotation": data.parent, + "supplier_part_no": frappe.db.get_value("Item Supplier", + {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no") }) + sq_doc.append('items', args) + @frappe.whitelist() def get_pdf(doctype, name, supplier): doc = get_rfq_doc(doctype, name, supplier) diff --git a/erpnext/accounts/accounts b/erpnext/buying/report/subcontract_order_summary/__init__.py similarity index 100% rename from erpnext/accounts/accounts rename to erpnext/buying/report/subcontract_order_summary/__init__.py diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js new file mode 100644 index 00000000000..5ba52f1b21e --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js @@ -0,0 +1,45 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Subcontract Order Summary"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1 + }, + { + label: __("Purchase Order"), + fieldname: "name", + fieldtype: "Link", + options: "Purchase Order", + get_query: function() { + return { + filters: { + docstatus: 1, + is_subcontracted: 'Yes', + company: frappe.query_report.get_filter_value('company') + } + } + } + } + ] +}; diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json new file mode 100644 index 00000000000..526a8d8ad01 --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-05-31 14:43:32.417694", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-05-31 14:43:32.417694", + "modified_by": "Administrator", + "module": "Buying", + "name": "Subcontract Order Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Order", + "report_name": "Subcontract Order Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Purchase User" + } + ] +} \ No newline at end of file diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py new file mode 100644 index 00000000000..0c0d4f0531d --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -0,0 +1,152 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + + return columns, data + +def get_data(report_filters): + data = [] + orders = get_subcontracted_orders(report_filters) + + if orders: + supplied_items = get_supplied_items(orders, report_filters) + po_details = prepare_subcontracted_data(orders, supplied_items) + get_subcontracted_data(po_details, data) + + return data + +def get_subcontracted_orders(report_filters): + fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`', + '`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`', + '`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`'] + + filters = get_filters(report_filters) + + return frappe.get_all('Purchase Order', fields = fields, filters=filters) or [] + +def get_filters(report_filters): + filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'], + ['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]] + + for field in ['name', 'company']: + if report_filters.get(field): + filters.append(['Purchase Order', field, '=', report_filters.get(field)]) + + return filters + +def get_supplied_items(orders, report_filters): + if not orders: + return [] + + fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty', + 'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name'] + + filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1} + + supplied_items = {} + for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters): + new_key = (row.parent, row.reference_name, row.main_item_code) + + supplied_items.setdefault(new_key, []).append(row) + + return supplied_items + +def prepare_subcontracted_data(orders, supplied_items): + po_details = {} + for row in orders: + key = (row.po_id, row.name, row.item_code) + if key not in po_details: + po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []})) + + details = po_details[key] + + if supplied_items.get(key): + for supplied_item in supplied_items[key]: + details['supplied_items'].append(supplied_item) + + return po_details + +def get_subcontracted_data(po_details, data): + for key, details in po_details.items(): + res = details.po_item + for index, row in enumerate(details.supplied_items): + if index != 0: + res = {} + + res.update(row) + data.append(res) + +def get_columns(): + return [ + { + "label": _("Purchase Order"), + "fieldname": "po_id", + "fieldtype": "Link", + "options": "Purchase Order", + "width": 100 + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "width": 80 + }, + { + "label": _("Subcontracted Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 160 + }, + { + "label": _("Order Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 90 + }, + { + "label": _("Received Qty"), + "fieldname": "received_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Supplied Item"), + "fieldname": "rm_item_code", + "fieldtype": "Link", + "options": "Item", + "width": 160 + }, + { + "label": _("Required Qty"), + "fieldname": "required_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Supplied Qty"), + "fieldname": "supplied_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Consumed Qty"), + "fieldname": "consumed_qty", + "fieldtype": "Float", + "width": 120 + }, + { + "label": _("Returned Qty"), + "fieldname": "returned_qty", + "fieldtype": "Float", + "width": 110 + } + ] \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 243939b275f..1c086e9edcd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -828,8 +828,14 @@ class AccountsController(TransactionBase): role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles(): - frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") - .format(item.item_code, item.idx, max_allowed_amt)) + if self.doctype != "Purchase Invoice": + self.throw_overbill_exception(item, max_allowed_amt) + elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")): + self.throw_overbill_exception(item, max_allowed_amt) + + def throw_overbill_exception(self, item, max_allowed_amt): + frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") + .format(item.item_code, item.idx, max_allowed_amt)) def get_company_default(self, fieldname): from erpnext.accounts.utils import get_company_default diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index da819119b10..6a550e0e975 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details from erpnext.stock.get_item_details import get_conversion_factor from erpnext.buying.utils import validate_for_items, update_last_purchase_rate from erpnext.stock.stock_ledger import get_valuation_rate -from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos from frappe.contacts.doctype.address.address import get_address_display from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget -from erpnext.controllers.stock_controller import StockController from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.stock.utils import get_incoming_rate -class BuyingController(StockController): +from erpnext.controllers.stock_controller import StockController +from erpnext.controllers.subcontracting import Subcontracting + +class BuyingController(StockController, Subcontracting): def get_feed(self): if self.get("supplier_name"): @@ -57,6 +58,11 @@ class BuyingController(StockController): if self.doctype in ("Purchase Receipt", "Purchase Invoice"): self.update_valuation_rate() + def onload(self): + super(BuyingController, self).onload() + self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings', + 'backflush_raw_materials_of_subcontract_based_on')) + def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) @@ -171,12 +177,13 @@ class BuyingController(StockController): TODO: rename item_tax_amount to valuation_tax_amount """ + stock_and_asset_items = [] stock_and_asset_items = self.get_stock_items() + self.get_asset_items() stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 last_item_idx = 1 for d in self.get("items"): - if d.item_code and d.item_code in stock_and_asset_items: + if (d.item_code and d.item_code in stock_and_asset_items): stock_and_asset_items_qty += flt(d.qty) stock_and_asset_items_amount += flt(d.base_net_amount) last_item_idx = d.idx @@ -255,7 +262,7 @@ class BuyingController(StockController): supplied_items_cost = 0.0 for d in self.get("supplied_items"): if d.reference_name == item_row_id: - if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'): + if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'): rate = get_incoming_rate({ "item_code": d.rm_item_code, "warehouse": self.supplier_warehouse, @@ -285,11 +292,13 @@ class BuyingController(StockController): if item in self.sub_contracted_items and not item.bom: frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) - if self.doctype == "Purchase Order": - for supplied_item in self.get("supplied_items"): - if not supplied_item.reserve_warehouse: - frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code))) + if self.doctype != "Purchase Order": + return + for row in self.get("supplied_items"): + if not row.reserve_warehouse: + msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" + frappe.throw(_(msg)) else: for item in self.get("items"): if item.bom: @@ -297,23 +306,7 @@ class BuyingController(StockController): def create_raw_materials_supplied(self, raw_material_table): if self.is_subcontracted=="Yes": - parent_items = [] - backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings", - "backflush_raw_materials_of_subcontract_based_on") - if (self.doctype == 'Purchase Receipt' and - backflush_raw_materials_based_on != 'BOM'): - self.update_raw_materials_supplied_based_on_stock_entries() - else: - for item in self.get("items"): - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - item.rm_supp_cost = 0.0 - if item.bom and item.item_code in self.sub_contracted_items: - self.update_raw_materials_supplied_based_on_bom(item, raw_material_table) - - if [item.item_code, item.name] not in parent_items: - parent_items.append([item.item_code, item.name]) - - self.cleanup_raw_materials_supplied(parent_items, raw_material_table) + self.set_materials_for_subcontracted_items(raw_material_table) elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: for item in self.get("items"): @@ -322,176 +315,6 @@ class BuyingController(StockController): if self.is_subcontracted == "No" and self.get("supplied_items"): self.set('supplied_items', []) - def update_raw_materials_supplied_based_on_stock_entries(self): - self.set('supplied_items', []) - - purchase_orders = set(d.purchase_order for d in self.items) - - # qty of raw materials backflushed (for each item per purchase order) - backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders) - - # qty of "finished good" item yet to be received - qty_to_be_received_map = get_qty_to_be_received(purchase_orders) - - for item in self.get('items'): - if not item.purchase_order: - continue - - # reset raw_material cost - item.rm_supp_cost = 0 - - # qty of raw materials transferred to the supplier - transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code) - - non_stock_items = get_non_stock_items(item.purchase_order, item.item_code) - - item_key = '{}{}'.format(item.item_code, item.purchase_order) - - fg_yet_to_be_received = qty_to_be_received_map.get(item_key) - - if not fg_yet_to_be_received: - frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}") - .format(item.idx, frappe.bold(item.item_code), - frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)), - title=_("Limit Crossed")) - - transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) - # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) - - for raw_material in transferred_raw_materials + non_stock_items: - rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order) - raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) - - consumed_qty = raw_material_data.get('qty', 0) - consumed_serial_nos = raw_material_data.get('serial_no', '') - consumed_batch_nos = raw_material_data.get('batch_nos', '') - - transferred_qty = raw_material.qty - - rm_qty_to_be_consumed = transferred_qty - consumed_qty - - # backflush all remaining transferred qty in the last Purchase Receipt - if fg_yet_to_be_received == item.qty: - qty = rm_qty_to_be_consumed - else: - qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty - - if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'): - qty = frappe.utils.ceil(qty) - - if qty > rm_qty_to_be_consumed: - qty = rm_qty_to_be_consumed - - if not qty: continue - - if raw_material.serial_nos: - set_serial_nos(raw_material, consumed_serial_nos, qty) - - if raw_material.batch_nos: - backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {}) - - batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, - qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order) - - for batch_data in batches_qty: - qty = batch_data['qty'] - raw_material.batch_no = batch_data['batch'] - if qty > 0: - self.append_raw_material_to_be_backflushed(item, raw_material, qty) - else: - self.append_raw_material_to_be_backflushed(item, raw_material, qty) - - def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty): - rm = self.append('supplied_items', {}) - rm.update(raw_material_data) - - if not rm.main_item_code: - rm.main_item_code = fg_item_row.item_code - - rm.reference_name = fg_item_row.name - rm.required_qty = qty - rm.consumed_qty = qty - - def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table): - exploded_item = 1 - if hasattr(item, 'include_exploded_items'): - exploded_item = item.get('include_exploded_items') - - bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item) - - used_alternative_items = [] - if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order: - used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order) - - raw_materials_cost = 0 - items = list(set([d.item_code for d in bom_items])) - item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse - from `tabItem` i, `tabItem Default` id - where id.parent=i.name and id.company=%s and i.name in ({0})""" - .format(", ".join(["%s"] * len(items))), [self.company] + items)) - - for bom_item in bom_items: - if self.doctype == "Purchase Order": - reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code) - if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company: - reserve_warehouse = None - - conversion_factor = item.conversion_factor - if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and - bom_item.item_code in used_alternative_items): - alternative_item_data = used_alternative_items.get(bom_item.item_code) - bom_item.item_code = alternative_item_data.item_code - bom_item.item_name = alternative_item_data.item_name - bom_item.stock_uom = alternative_item_data.stock_uom - conversion_factor = alternative_item_data.conversion_factor - bom_item.description = alternative_item_data.description - - # check if exists - exists = 0 - for d in self.get(raw_material_table): - if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \ - and d.reference_name == item.name: - rm, exists = d, 1 - break - - if not exists: - rm = self.append(raw_material_table, {}) - - required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) * - flt(conversion_factor), rm.precision("required_qty")) - rm.reference_name = item.name - rm.bom_detail_no = bom_item.name - rm.main_item_code = item.item_code - rm.rm_item_code = bom_item.item_code - rm.stock_uom = bom_item.stock_uom - rm.required_qty = required_qty - rm.rate = bom_item.rate - rm.conversion_factor = conversion_factor - - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - rm.consumed_qty = required_qty - rm.description = bom_item.description - if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no: - rm.batch_no = item.batch_no - elif not rm.reserve_warehouse: - rm.reserve_warehouse = reserve_warehouse - - def cleanup_raw_materials_supplied(self, parent_items, raw_material_table): - """Remove all those child items which are no longer present in main item table""" - delete_list = [] - for d in self.get(raw_material_table): - if [d.main_item_code, d.reference_name] not in parent_items: - # mark for deletion from doclist - delete_list.append(d) - - # delete from doclist - if delete_list: - rm_supplied_details = self.get(raw_material_table) - self.set(raw_material_table, []) - for d in rm_supplied_details: - if d not in delete_list: - self.append(raw_material_table, d) - @property def sub_contracted_items(self): if not hasattr(self, "_sub_contracted_items"): @@ -683,7 +506,8 @@ class BuyingController(StockController): self.process_fixed_asset() self.update_fixed_asset(field) - update_last_purchase_rate(self, is_submit = 1) + if self.doctype in ['Purchase Order', 'Purchase Receipt']: + update_last_purchase_rate(self, is_submit = 1) def on_cancel(self): super(BuyingController, self).on_cancel() @@ -691,7 +515,9 @@ class BuyingController(StockController): if self.get('is_return'): return - update_last_purchase_rate(self, is_submit = 0) + if self.doctype in ['Purchase Order', 'Purchase Receipt']: + update_last_purchase_rate(self, is_submit = 0) + if self.doctype in ['Purchase Receipt', 'Purchase Invoice']: field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt' @@ -863,104 +689,6 @@ class BuyingController(StockController): else: validate_item_type(self, "is_purchase_item", "purchase") - -def get_items_from_bom(item_code, bom, exploded_item=1): - doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" - - bom_items = frappe.db.sql("""select t2.item_code, t2.name, - t2.rate, t2.stock_uom, t2.source_warehouse, t2.description, - t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit - from - `tabBOM` t1, `tab{0}` t2, tabItem t3 - where - t2.parent = t1.name and t1.item = %s - and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s - and t2.sourced_by_supplier = 0 - and t2.item_code = t3.name""".format(doctype), - (item_code, bom), as_dict=1) - - if not bom_items: - msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1) - - return bom_items - -def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): - common_query = """ - SELECT - sed.item_code AS rm_item_code, - SUM(sed.qty) AS qty, - sed.description, - sed.stock_uom, - sed.subcontracted_item AS main_item_code, - {serial_no_concat_syntax} AS serial_nos, - {batch_no_concat_syntax} AS batch_nos - FROM `tabStock Entry` se,`tabStock Entry Detail` sed - WHERE - se.name = sed.parent - AND se.docstatus=1 - AND se.purpose='Send to Subcontractor' - AND se.purchase_order = %s - AND IFNULL(sed.t_warehouse, '') != '' - AND IFNULL(sed.subcontracted_item, '') in ('', %s) - GROUP BY sed.item_code, sed.subcontracted_item - """ - raw_materials = frappe.db.multisql({ - 'mariadb': common_query.format( - serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)", - batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)" - ), - 'postgres': common_query.format( - serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')", - batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')" - ) - }, (purchase_order, fg_item), as_dict=1) - - return raw_materials - -def get_backflushed_subcontracted_raw_materials(purchase_orders): - purchase_receipts = frappe.get_all("Purchase Receipt Item", - fields = ["purchase_order", "item_code", "name", "parent"], - filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))}) - - distinct_purchase_receipts = {} - for pr in purchase_receipts: - key = (pr.purchase_order, pr.item_code, pr.parent) - distinct_purchase_receipts.setdefault(key, []).append(pr.name) - - backflushed_raw_materials_map = frappe._dict() - for args, references in iteritems(distinct_purchase_receipts): - purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references) - - for data in purchase_receipt_supplied_items: - pr_key = (data.rm_item_code, data.main_item_code, args[0]) - if pr_key not in backflushed_raw_materials_map: - backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({ - "qty": 0.0, - "serial_no": [], - "batch_no": [], - "consumed_batch": {} - })) - - row = backflushed_raw_materials_map.get(pr_key) - row.qty += data.consumed_qty - - for field in ["serial_no", "batch_no"]: - if data.get(field): - row[field].append(data.get(field)) - - if data.get("batch_no"): - if data.get("batch_no") in row.consumed_batch: - row.consumed_batch[data.get("batch_no")] += data.consumed_qty - else: - row.consumed_batch[data.get("batch_no")] = data.consumed_qty - - return backflushed_raw_materials_map - -def get_supplied_items(item_code, purchase_receipt, references): - return frappe.get_all("Purchase Receipt Item Supplied", - fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"], - filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)}) - def get_asset_item_details(asset_items): asset_items_data = {} for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], @@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message): error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message) frappe.throw(error_message) - -def get_qty_to_be_received(purchase_orders): - return frappe._dict(frappe.db.sql(""" - SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key, - SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received - FROM `tabPurchase Order Item` poi - WHERE - poi.`parent` in %s - GROUP BY poi.`item_code`, poi.`parent` - HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`) - """, (purchase_orders))) - -def get_non_stock_items(purchase_order, fg_item_code): - return frappe.db.sql(""" - SELECT - pois.main_item_code, - pois.rm_item_code, - item.description, - pois.required_qty AS qty, - pois.rate, - 1 as non_stock_item, - pois.stock_uom - FROM `tabPurchase Order Item Supplied` pois, `tabItem` item - WHERE - pois.`rm_item_code` = item.`name` - AND item.is_stock_item = 0 - AND pois.`parent` = %s - AND pois.`main_item_code` = %s - """, (purchase_order, fg_item_code), as_dict=1) - - -def set_serial_nos(raw_material, consumed_serial_nos, qty): - serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \ - set(get_serial_nos(consumed_serial_nos)) - if serial_nos and qty <= len(serial_nos): - raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)]) - -def get_transferred_batch_qty_map(purchase_order, fg_item): - # returns - # { - # (item_code, fg_code): { - # batch1: 10, # qty - # batch2: 16 - # }, - # } - transferred_batch_qty_map = {} - transferred_batches = frappe.db.sql(""" - SELECT - sed.batch_no, - SUM(sed.qty) AS qty, - sed.item_code, - sed.subcontracted_item - FROM `tabStock Entry` se,`tabStock Entry Detail` sed - WHERE - se.name = sed.parent - AND se.docstatus=1 - AND se.purpose='Send to Subcontractor' - AND se.purchase_order = %s - AND ifnull(sed.subcontracted_item, '') in ('', %s) - AND sed.batch_no IS NOT NULL - GROUP BY - sed.batch_no, - sed.item_code - """, (purchase_order, fg_item), as_dict=1) - - for batch_data in transferred_batches: - key = ((batch_data.item_code, fg_item) - if batch_data.subcontracted_item else (batch_data.item_code, purchase_order)) - transferred_batch_qty_map.setdefault(key, OrderedDict()) - transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty - - return transferred_batch_qty_map - -def get_backflushed_batch_qty_map(purchase_order, fg_item): - # returns - # { - # (item_code, fg_code): { - # batch1: 10, # qty - # batch2: 16 - # }, - # } - backflushed_batch_qty_map = {} - backflushed_batches = frappe.db.sql(""" - SELECT - pris.batch_no, - SUM(pris.consumed_qty) AS qty, - pris.rm_item_code AS item_code - FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris - WHERE - pr.name = pri.parent - AND pri.parent = pris.parent - AND pri.purchase_order = %s - AND pri.item_code = pris.main_item_code - AND pr.docstatus = 1 - AND pris.main_item_code = %s - AND pris.batch_no IS NOT NULL - GROUP BY - pris.rm_item_code, pris.batch_no - """, (purchase_order, fg_item), as_dict=1) - - for batch_data in backflushed_batches: - backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {}) - backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty - - return backflushed_batch_qty_map - -def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po): - # Returns available batches to be backflushed based on requirements - transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {}) - if not transferred_batches: - transferred_batches = transferred_batch_qty_map.get((item_code, po), {}) - - available_batches = [] - - for (batch, transferred_qty) in transferred_batches.items(): - backflushed_qty = backflushed_batches.get(batch, 0) - available_qty = transferred_qty - backflushed_qty - - if available_qty >= required_qty: - available_batches.append({'batch': batch, 'qty': required_qty}) - break - elif available_qty != 0: - available_batches.append({'batch': batch, 'qty': available_qty}) - required_qty -= available_qty - - for row in available_batches: - if backflushed_batches.get(row.get('batch'), 0) > 0: - backflushed_batches[row.get('batch')] += row.get('qty') - else: - backflushed_batches[row.get('batch')] = row.get('qty') - - return available_batches diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 7bd739a6ad3..280319321f2 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Employee", ["name", "employee_name"]) return frappe.db.sql("""select {fields} from `tabEmployee` - where status = 'Active' + where status in ('Active', 'Suspended') and docstatus < 2 and ({key} like %(txt)s or employee_name like %(txt)s) @@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select {fields} from `tabProject` where `tabProject`.status not in ("Completed", "Cancelled") - and {cond} {match_cond} {scond} + and {cond} {scond} {match_cond} order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), idx desc, diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5f759b43bc6..80ccc6d75b2 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -99,9 +99,10 @@ def validate_returned_items(doc): frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}") .format(d.idx, s, doc.doctype, doc.return_against)) - if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \ - and not d.get("warehouse"): - frappe.throw(_("Warehouse is mandatory")) + if (warehouse_mandatory and not d.get("warehouse") and + frappe.db.get_value("Item", d.item_code, "is_stock_item") + ): + frappe.throw(_("Warehouse is mandatory")) items_returned = True @@ -462,4 +463,4 @@ def get_returned_serial_nos(child_doc, parent_doc): for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters): serial_nos.extend(get_serial_nos(row.serial_no)) - return serial_nos \ No newline at end of file + return serial_nos diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7f28289760c..da2765deded 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -330,9 +330,15 @@ class SellingController(StockController): # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): - rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) - if d.rate != rate: - d.rate = rate + if d.doctype == "Packed Item": + incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate')) + if d.incoming_rate != incoming_rate: + d.incoming_rate = incoming_rate + else: + rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + if d.rate != rate: + d.rate = rate + d.discount_percentage = 0 d.discount_amount = 0 frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 6a7c9e3d0e7..8196cff849d 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map -from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map from erpnext.stock.stock_ledger import get_valuation_rate @@ -497,10 +497,6 @@ class StockController(AccountsController): }) if future_sle_exists(args): create_repost_item_valuation_entry(args) - elif not is_reposting_pending(): - check_if_stock_and_account_balance_synced(self.posting_date, - self.company, self.doctype, self.name) - @frappe.whitelist() def make_quality_inspections(doctype, docname, items): @@ -533,21 +529,75 @@ def make_quality_inspections(doctype, docname, items): return inspections - def is_reposting_pending(): return frappe.db.exists("Repost Item Valuation", {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) +def future_sle_exists(args, sl_entries=None): + key = (args.voucher_type, args.voucher_no) -def future_sle_exists(args): - sl_entries = frappe.get_all("Stock Ledger Entry", + if validate_future_sle_not_exists(args, key, sl_entries): + return False + elif get_cached_data(args, key): + return True + + if not sl_entries: + sl_entries = get_sle_entries_against_voucher(args) + if not sl_entries: + return + + or_conditions = get_conditions_to_validate_future_sle(sl_entries) + + data = frappe.db.sql(""" + select item_code, warehouse, count(name) as total_row + from `tabStock Ledger Entry` + where + ({}) + and timestamp(posting_date, posting_time) + >= timestamp(%(posting_date)s, %(posting_time)s) + and voucher_no != %(voucher_no)s + and is_cancelled = 0 + GROUP BY + item_code, warehouse + """.format(" or ".join(or_conditions)), args, as_dict=1) + + for d in data: + frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row + + return len(data) + +def validate_future_sle_not_exists(args, key, sl_entries=None): + item_key = '' + if args.get('item_code'): + item_key = (args.get('item_code'), args.get('warehouse')) + + if not sl_entries and hasattr(frappe.local, 'future_sle'): + if (not frappe.local.future_sle.get(key) or + (item_key and item_key not in frappe.local.future_sle.get(key))): + return True + +def get_cached_data(args, key): + if not hasattr(frappe.local, 'future_sle'): + frappe.local.future_sle = {} + + if key not in frappe.local.future_sle: + frappe.local.future_sle[key] = frappe._dict({}) + + if args.get('item_code'): + item_key = (args.get('item_code'), args.get('warehouse')) + count = frappe.local.future_sle[key].get(item_key) + + return True if (count or count == 0) else False + else: + return frappe.local.future_sle[key] + +def get_sle_entries_against_voucher(args): + return frappe.get_all("Stock Ledger Entry", filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, fields=["item_code", "warehouse"], order_by="creation asc") - if not sl_entries: - return - +def get_conditions_to_validate_future_sle(sl_entries): warehouse_items_map = {} for entry in sl_entries: if entry.warehouse not in warehouse_items_map: @@ -561,17 +611,7 @@ def future_sle_exists(args): f"""warehouse = {frappe.db.escape(warehouse)} and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""") - return frappe.db.sql(""" - select name - from `tabStock Ledger Entry` - where - ({}) - and timestamp(posting_date, posting_time) - >= timestamp(%(posting_date)s, %(posting_time)s) - and voucher_no != %(voucher_no)s - and is_cancelled = 0 - limit 1 - """.format(" or ".join(or_conditions)), args) + return or_conditions def create_repost_item_valuation_entry(args): args = frappe._dict(args) diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py new file mode 100644 index 00000000000..36ae1102164 --- /dev/null +++ b/erpnext/controllers/subcontracting.py @@ -0,0 +1,393 @@ +import frappe +import copy +from frappe import _ +from frappe.utils import flt, cint, get_link_to_form +from collections import defaultdict +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + +class Subcontracting(): + def set_materials_for_subcontracted_items(self, raw_material_table): + if self.doctype == 'Purchase Invoice' and not self.update_stock: + return + + self.raw_material_table = raw_material_table + self.__identify_change_in_item_table() + self.__prepare_supplied_items() + self.__validate_supplied_items() + + def __prepare_supplied_items(self): + self.initialized_fields() + self.__get_purchase_orders() + self.__get_pending_qty_to_receive() + self.get_available_materials() + self.__remove_changed_rows() + self.__set_supplied_items() + + def initialized_fields(self): + self.available_materials = frappe._dict() + self.__transferred_items = frappe._dict() + self.alternative_item_details = frappe._dict() + self.__get_backflush_based_on() + + def __get_backflush_based_on(self): + self.backflush_based_on = frappe.db.get_single_value("Buying Settings", + "backflush_raw_materials_of_subcontract_based_on") + + def __get_purchase_orders(self): + self.purchase_orders = [] + + if self.doctype == 'Purchase Order': + return + + self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order] + + def __identify_change_in_item_table(self): + self.__changed_name = [] + self.__reference_name = [] + + if self.doctype == 'Purchase Order' or self.is_new(): + self.set(self.raw_material_table, []) + return + + item_dict = self.__get_data_before_save() + if not item_dict: + return True + + for n_row in self.items: + self.__reference_name.append(n_row.name) + if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]: + self.__changed_name.append(n_row.name) + + if item_dict.get(n_row.name): + del item_dict[n_row.name] + + self.__changed_name.extend(item_dict.keys()) + + def __get_data_before_save(self): + item_dict = {} + if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save: + for row in self._doc_before_save.get('items'): + item_dict[row.name] = (row.item_code, row.qty) + + return item_dict + + def get_available_materials(self): + ''' Get the available raw materials which has been transferred to the supplier. + available_materials = { + (item_code, subcontracted_item, purchase_order): { + 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details + } + } + ''' + if not self.purchase_orders: + return + + for row in self.__get_transferred_items(): + key = (row.rm_item_code, row.main_item_code, row.purchase_order) + + if key not in self.available_materials: + self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [], + 'batch_no': defaultdict(float), 'item_details': row, 'po_details': []}) + ) + + details = self.available_materials[key] + details.qty += row.qty + details.po_details.append(row.po_detail) + + if row.serial_no: + details.serial_no.extend(get_serial_nos(row.serial_no)) + + if row.batch_no: + details.batch_no[row.batch_no] += row.qty + + self.__set_alternative_item_details(row) + + self.__transferred_items = copy.deepcopy(self.available_materials) + for doctype in ['Purchase Receipt', 'Purchase Invoice']: + self.__update_consumed_materials(doctype) + + def __update_consumed_materials(self, doctype, return_consumed_items=False): + '''Deduct the consumed materials from the available materials.''' + + pr_items = self.__get_received_items(doctype) + if not pr_items: + return ([], {}) if return_consumed_items else None + + pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items} + consumed_materials = self.__get_consumed_items(doctype, pr_items.keys()) + + if return_consumed_items: + return (consumed_materials, pr_items) + + for row in consumed_materials: + key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) + if not self.available_materials.get(key): + continue + + self.available_materials[key]['qty'] -= row.consumed_qty + if row.serial_no: + self.available_materials[key]['serial_no'] = list( + set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no)) + ) + + if row.batch_no: + self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty + + def __get_transferred_items(self): + fields = ['`tabStock Entry`.`purchase_order`'] + alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'} + + child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount', + 'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor', + 's_warehouse', 't_warehouse', 'item_group', 'po_detail'] + + if self.backflush_based_on == 'BOM': + child_table_fields.append('original_item') + + for field in child_table_fields: + fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}') + + filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'], + ['Stock Entry', 'purchase_order', 'in', self.purchase_orders]] + + return frappe.get_all('Stock Entry', fields = fields, filters=filters) + + def __get_received_items(self, doctype): + fields = [] + self.po_field = 'purchase_order' + + for field in ['name', self.po_field, 'parent']: + fields.append(f'`tab{doctype} Item`.`{field}`') + + filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]] + if doctype == 'Purchase Invoice': + filters.append(['Purchase Invoice', 'update_stock', "=", 1]) + + return frappe.get_all(f'{doctype}', fields = fields, filters = filters) + + def __get_consumed_items(self, doctype, pr_items): + return frappe.get_all('Purchase Receipt Item Supplied', + fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'], + filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype}) + + def __set_alternative_item_details(self, row): + if row.get('original_item'): + self.alternative_item_details[row.get('original_item')] = row + + def __get_pending_qty_to_receive(self): + '''Get qty to be received against the purchase order.''' + + self.qty_to_be_received = defaultdict(float) + + if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders: + for row in frappe.get_all('Purchase Order Item', + fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'], + filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}): + + self.qty_to_be_received[(row.item_code, row.parent)] += row.qty + + def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item' + fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit'] + + alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'} + for field in ['item_code', 'name', 'rate', 'stock_uom', + 'source_warehouse', 'description', 'item_name', 'stock_uom']: + fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}') + + filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1], + ['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]] + + return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or [] + + def __remove_changed_rows(self): + if not self.__changed_name: + return + + i=1 + self.set(self.raw_material_table, []) + for d in self._doc_before_save.supplied_items: + if d.reference_name in self.__changed_name: + continue + + if (d.reference_name not in self.__reference_name): + continue + + d.idx = i + self.append('supplied_items', d) + + i += 1 + + def __set_supplied_items(self): + self.bom_items = {} + + has_supplied_items = True if self.get(self.raw_material_table) else False + for row in self.items: + if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name) + or (has_supplied_items and not self.__changed_name))): + continue + + if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM': + for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')): + qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor) + bom_item.main_item_code = row.item_code + self.__update_reserve_warehouse(bom_item, row) + self.__set_alternative_item(bom_item) + self.__add_supplied_item(row, bom_item, qty) + + elif self.backflush_based_on != 'BOM': + for key, transfer_item in self.available_materials.items(): + if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0: + qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0 + transfer_item.qty -= qty + self.__add_supplied_item(row, transfer_item.get('item_details'), qty) + + if self.qty_to_be_received: + self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty + + def __update_reserve_warehouse(self, row, item): + if self.doctype == 'Purchase Order': + row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse) + + def __get_qty_based_on_material_transfer(self, item_row, transfer_item): + key = (item_row.item_code, item_row.purchase_order) + + if self.qty_to_be_received == item_row.qty: + return transfer_item.qty + + if self.qty_to_be_received: + qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + transfer_item.item_details.required_qty = transfer_item.qty + + if (transfer_item.serial_no or frappe.get_cached_value('UOM', + transfer_item.item_details.stock_uom, 'must_be_whole_number')): + return frappe.utils.ceil(qty) + + return qty + + def __set_alternative_item(self, bom_item): + if self.alternative_item_details.get(bom_item.rm_item_code): + bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) + + def __add_supplied_item(self, item_row, bom_item, qty): + bom_item.conversion_factor = item_row.conversion_factor + rm_obj = self.append(self.raw_material_table, bom_item) + rm_obj.reference_name = item_row.name + + if self.doctype == 'Purchase Order': + rm_obj.required_qty = qty + else: + rm_obj.consumed_qty = 0 + rm_obj.purchase_order = item_row.purchase_order + self.__set_batch_nos(bom_item, item_row, rm_obj, qty) + + def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) + + if (self.available_materials.get(key) and self.available_materials[key]['batch_no']): + new_rm_obj = None + for batch_no, batch_qty in self.available_materials[key]['batch_no'].items(): + if batch_qty >= qty: + self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) + self.available_materials[key]['batch_no'][batch_no] -= qty + return + + elif qty > 0 and batch_qty > 0: + qty -= batch_qty + new_rm_obj = self.append(self.raw_material_table, bom_item) + new_rm_obj.reference_name = item_row.name + self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) + self.available_materials[key]['batch_no'][batch_no] = 0 + + if abs(qty) > 0 and not new_rm_obj: + self.__set_consumed_qty(rm_obj, qty) + else: + self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) + self.__set_serial_nos(item_row, rm_obj) + + def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0): + rm_obj.required_qty = required_qty + rm_obj.consumed_qty = consumed_qty + + def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): + rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, + 'required_qty': qty, 'purchase_order': item_row.purchase_order}) + + self.__set_serial_nos(item_row, rm_obj) + + def __set_serial_nos(self, item_row, rm_obj): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) + if (self.available_materials.get(key) and self.available_materials[key]['serial_no']): + used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)] + rm_obj.serial_no = '\n'.join(used_serial_nos) + + # Removed the used serial nos from the list + for sn in used_serial_nos: + self.available_materials[key]['serial_no'].remove(sn) + + def set_consumed_qty_in_po(self): + # Update consumed qty back in the purchase order + if self.is_subcontracted != 'Yes': + return + + self.__get_purchase_orders() + itemwise_consumed_qty = defaultdict(float) + for doctype in ['Purchase Receipt', 'Purchase Invoice']: + consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True) + + for row in consumed_items: + key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) + itemwise_consumed_qty[key] += row.consumed_qty + + self.__update_consumed_qty_in_po(itemwise_consumed_qty) + + def __update_consumed_qty_in_po(self, itemwise_consumed_qty): + fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name'] + filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)} + + for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'): + key = (row.rm_item_code, row.main_item_code, row.parent) + consumed_qty = itemwise_consumed_qty.get(key, 0) + + if row.supplied_qty < consumed_qty: + consumed_qty = row.supplied_qty + + itemwise_consumed_qty[key] -= consumed_qty + frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty) + + def __validate_supplied_items(self): + if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']: + return + + for row in self.get(self.raw_material_table): + self.__validate_consumed_qty(row) + + key = (row.rm_item_code, row.main_item_code, row.purchase_order) + if not self.__transferred_items or not self.__transferred_items.get(key): + return + + self.__validate_batch_no(row, key) + self.__validate_serial_no(row, key) + + def __validate_consumed_qty(self, row): + if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0: + msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' + + frappe.throw(_(msg),title=_('Consumed Items Qty Check')) + + def __validate_batch_no(self, row, key): + if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'): + link = get_link_to_form('Purchase Order', row.purchase_order) + msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}' + frappe.throw(_(msg), title=_("Incorrect Batch Consumed")) + + def __validate_serial_no(self, row, key): + if row.get('serial_no'): + serial_nos = get_serial_nos(row.get('serial_no')) + incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no')) + + if incorrect_sn: + incorrect_sn = "\n".join(incorrect_sn) + link = get_link_to_form('Purchase Order', row.purchase_order) + msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}' + frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) \ No newline at end of file diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2bb83ea7f0f..56da5b71da0 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object): item.margin_type = None item.margin_rate_or_amount = 0.0 - if item.margin_type and item.margin_rate_or_amount: + if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate): + item.margin_type = "Amount" + item.margin_rate_or_amount = flt(item.rate - item.price_list_rate, + item.precision("margin_rate_or_amount")) + item.rate_with_margin = item.rate + + elif item.margin_type and item.margin_rate_or_amount: margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 rate_with_margin = flt(item.price_list_rate) + flt(margin_value) base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 8517ddec321..306be7faa74 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -102,7 +102,7 @@ } ], "links": [], - "modified": "2020-01-28 16:16:45.447213", + "modified": "2021-06-29 18:27:02.832979", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", @@ -153,6 +153,18 @@ "role": "Sales User", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "write": 1 } ], "quick_entry": 1, diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index d1d096843bf..ce3de40fc3d 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -168,12 +168,13 @@ class Lead(SellingController): if self.phone: contact.append("phone_nos", { "phone": self.phone, - "is_primary": 1 + "is_primary_phone": 1 }) if self.mobile_no: contact.append("phone_nos", { - "phone": self.mobile_no + "phone": self.mobile_no, + "is_primary_mobile_no":1 }) contact.insert(ignore_permissions=True) diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js index 3f5c95ab0aa..fe5707af296 100644 --- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js @@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = { get_chart_data: function (_columns, result) { return { data: { - labels: result.map(d => d[0]), + labels: result.map(d => d.creation_date), datasets: [{ name: "First Response Time", - values: result.map(d => d[1]) + values: result.map(d => d.first_response_time) }] }, type: "line", @@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = { hide_days: 0, hide_seconds: 0 }; - value = frappe.utils.get_formatted_duration(d, duration_options); - return value; + return frappe.utils.get_formatted_duration(d, duration_options); } } } diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py index 9db8a4a90df..3070e6a3e8a 100644 --- a/erpnext/education/utils.py +++ b/erpnext/education/utils.py @@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program): student = get_current_student() course_enrollment = get_enrollment("course", course, student.name) if not course_enrollment: - program_enrollment = get_enrollment('program', program, student.name) + program_enrollment = get_enrollment('program', program.name, student.name) if not program_enrollment: frappe.throw(_("You are not enrolled in program {0}").format(program)) return - return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name)) + return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name)) else: return frappe.get_doc('Course Enrollment', course_enrollment) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 8ad77a1524d..52daec91805 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -157,6 +157,7 @@ website_route_rules = [ "parents": [{"label": _("Material Request"), "route": "material-requests"}] } }, + {"from_route": "/project", "to_route": "Project"} ] standard_portal_menu_items = [ diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js index c3c3cb82f94..7964078c7f0 100644 --- a/erpnext/hr/doctype/attendance/attendance.js +++ b/erpnext/hr/doctype/attendance/attendance.js @@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) { cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { return{ query: "erpnext.controllers.queries.employee_query" - } + } } diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index f3b8a799b3c..3412675d811 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -15,6 +15,7 @@ class Attendance(Document): validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) self.validate_attendance_date() self.validate_duplicate_record() + self.validate_employee_status() self.check_leave_record() def validate_attendance_date(self): @@ -38,6 +39,10 @@ class Attendance(Document): frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( frappe.bold(self.employee), frappe.bold(self.attendance_date))) + def validate_employee_status(self): + if frappe.db.get_value("Employee", self.employee, "status") == "Inactive": + frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee)) + def check_leave_record(self): leave_record = frappe.db.sql(""" select leave_type, half_day, half_day_date diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 0c7eafe9c61..9a3bac0eb23 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = { label: __('For Employee'), fieldtype: 'Link', options: 'Employee', + get_query: () => { + return {query: "erpnext.controllers.queries.employee_query"} + }, reqd: 1, onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 5442ed56c31..d592a9c79e2 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -207,7 +207,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Active\nInactive\nLeft", + "options": "Active\nInactive\nSuspended\nLeft", "reqd": 1, "search_index": 1 }, @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2021-06-12 11:31:37.730760", + "modified": "2021-06-17 11:31:37.730760", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index bc5694226ad..fa017d9d4c2 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr +from frappe.utils import getdate, validate_email_address, today, add_years, cstr from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ @@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.model.document import Document from erpnext.utilities.transaction_base import delete_events from frappe.utils.nestedset import NestedSet -from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass @@ -37,7 +36,7 @@ class Employee(NestedSet): def validate(self): from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Active", "Inactive", "Left"]) + validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"]) self.employee = self.name self.set_employee_name() diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index 285374d9f69..e853bee69f2 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -7,7 +7,8 @@ def get_data(): 'heatmap_message': _('This is based on the attendance of this Employee'), 'fieldname': 'employee', 'non_standard_fieldnames': { - 'Bank Account': 'party' + 'Bank Account': 'party', + 'Employee Grievance': 'raised_by' }, 'transactions': [ { @@ -20,7 +21,7 @@ def get_data(): }, { 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation'] + 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance'] }, { 'label': _('Shift'), diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js index 6679e318c24..d37e1496ca3 100644 --- a/erpnext/hr/doctype/employee/employee_list.js +++ b/erpnext/hr/doctype/employee/employee_list.js @@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = { filters: [["status","=", "Active"]], get_indicator: function(doc) { var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; + indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status]; return indicator; } }; diff --git a/erpnext/hr/doctype/employee_grievance/__init__.py b/erpnext/hr/doctype/employee_grievance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.js b/erpnext/hr/doctype/employee_grievance/employee_grievance.js new file mode 100644 index 00000000000..25c5badbc77 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.js @@ -0,0 +1,39 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Employee Grievance', { + setup: function(frm) { + frm.set_query('grievance_against_party', function() { + return { + filters: { + name: ['in', [ + 'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee'] + ] + } + }; + }); + frm.set_query('associated_document_type', function() { + let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website", + "Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"]; + return { + filters: { + istable: 0, + issingle: 0, + module: ["Not In", ignore_modules] + } + }; + }); + }, + + grievance_against_party: function(frm) { + let filters = {}; + if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) { + filters.name = ["!=", frm.doc.raised_by]; + } + frm.set_query('grievance_against', function() { + return { + filters: filters + }; + }); + }, +}); diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.json b/erpnext/hr/doctype/employee_grievance/employee_grievance.json new file mode 100644 index 00000000000..5a918562afb --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.json @@ -0,0 +1,261 @@ +{ + "actions": [], + "autoname": "HR-GRIEV-.YYYY.-.#####", + "creation": "2021-05-11 13:41:51.485295", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "subject", + "raised_by", + "employee_name", + "designation", + "column_break_3", + "date", + "status", + "reports_to", + "grievance_details_section", + "grievance_against_party", + "grievance_against", + "grievance_type", + "column_break_11", + "associated_document_type", + "associated_document", + "section_break_14", + "description", + "investigation_details_section", + "cause_of_grievance", + "resolution_details_section", + "resolved_by", + "resolution_date", + "employee_responsible", + "column_break_16", + "resolution_detail", + "amended_from" + ], + "fields": [ + { + "fieldname": "grievance_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Grievance Type", + "options": "Grievance Type", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date ", + "reqd": 1 + }, + { + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Open\nInvestigated\nResolved\nInvalid", + "reqd": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description", + "reqd": 1 + }, + { + "fieldname": "cause_of_grievance", + "fieldtype": "Text", + "label": "Cause of Grievance", + "mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\"" + }, + { + "fieldname": "resolution_details_section", + "fieldtype": "Section Break", + "label": "Resolution Details" + }, + { + "fieldname": "resolved_by", + "fieldtype": "Link", + "label": "Resolved By", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"", + "options": "User" + }, + { + "fieldname": "employee_responsible", + "fieldtype": "Link", + "label": "Employee Responsible ", + "options": "Employee" + }, + { + "fieldname": "resolution_detail", + "fieldtype": "Small Text", + "label": "Resolution Details", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "resolution_date", + "fieldtype": "Date", + "label": "Resolution Date", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"" + }, + { + "fieldname": "grievance_against", + "fieldtype": "Dynamic Link", + "label": "Grievance Against", + "options": "grievance_against_party", + "reqd": 1 + }, + { + "fieldname": "raised_by", + "fieldtype": "Link", + "label": "Raised By", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Grievance", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "raised_by.designation", + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "read_only": 1 + }, + { + "fetch_from": "raised_by.reports_to", + "fieldname": "reports_to", + "fieldtype": "Link", + "label": "Reports To", + "options": "Employee", + "read_only": 1 + }, + { + "fieldname": "grievance_details_section", + "fieldtype": "Section Break", + "label": "Grievance Details" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_14", + "fieldtype": "Section Break" + }, + { + "fieldname": "grievance_against_party", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Grievance Against Party", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "associated_document_type", + "fieldtype": "Link", + "label": "Associated Document Type", + "options": "DocType" + }, + { + "fieldname": "associated_document", + "fieldtype": "Dynamic Link", + "label": "Associated Document", + "options": "associated_document_type" + }, + { + "fieldname": "investigation_details_section", + "fieldtype": "Section Break", + "label": "Investigation Details" + }, + { + "fetch_from": "raised_by.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-06-21 12:51:01.499486", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Grievance", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "search_fields": "subject,raised_by,grievance_against_party", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "subject", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py new file mode 100644 index 00000000000..503b5ea4449 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py @@ -0,0 +1,15 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, bold +from frappe.model.document import Document + +class EmployeeGrievance(Document): + def on_submit(self): + if self.status not in ["Invalid", "Resolved"]: + frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format( + bold("Invalid"), + bold("Resolved")) + ) + diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js new file mode 100644 index 00000000000..fc08e216099 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Employee Grievance"] = { + has_indicator_for_draft: 1, + get_indicator: function(doc) { + var colors = { + "Open": "red", + "Investigated": "orange", + "Resolved": "green", + "Invalid": "grey" + }; + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + } +}; \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py new file mode 100644 index 00000000000..a615b20a5a2 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py @@ -0,0 +1,51 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +import unittest +from frappe.utils import today +from erpnext.hr.doctype.employee.test_employee import make_employee +class TestEmployeeGrievance(unittest.TestCase): + def test_create_employee_grievance(self): + create_employee_grievance() + +def create_employee_grievance(): + grievance_type = create_grievance_type() + emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company") + emp_2 = make_employee("testculprit@example.com", company="_Test Company") + + grievance = frappe.new_doc("Employee Grievance") + grievance.subject = "Test Employee Grievance" + grievance.raised_by = emp_1 + grievance.date = today() + grievance.grievance_type = grievance_type + grievance.grievance_against_party = "Employee" + grievance.grievance_against = emp_2 + grievance.description = "test descrip" + + #set cause + grievance.cause_of_grievance = "test cause" + + #resolution details + grievance.resolution_date = today() + grievance.resolution_detail = "test resolution detail" + grievance.resolved_by = "test_emp_grievance_@example.com" + grievance.employee_responsible = emp_2 + grievance.status = "Resolved" + + grievance.save() + grievance.submit() + + return grievance + + +def create_grievance_type(): + if frappe.db.exists("Grievance Type", "Employee Abuse"): + return frappe.get_doc("Grievance Type", "Employee Abuse") + grievance_type = frappe.new_doc("Grievance Type") + grievance_type.name = "Employee Abuse" + grievance_type.description = "Test" + grievance_type.save() + + return grievance_type.name + diff --git a/erpnext/hr/doctype/grievance_type/__init__.py b/erpnext/hr/doctype/grievance_type/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.js b/erpnext/hr/doctype/grievance_type/grievance_type.js new file mode 100644 index 00000000000..425f2fd5b5e --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Grievance Type', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.json b/erpnext/hr/doctype/grievance_type/grievance_type.json new file mode 100644 index 00000000000..1dce00a0e22 --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2021-05-11 12:41:50.256071", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_5", + "description" + ], + "fields": [ + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-06-21 12:54:37.764712", + "modified_by": "Administrator", + "module": "HR", + "name": "Grievance Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.py b/erpnext/hr/doctype/grievance_type/grievance_type.py new file mode 100644 index 00000000000..618cf0a0318 --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class GrievanceType(Document): + pass diff --git a/erpnext/hr/doctype/grievance_type/test_grievance_type.py b/erpnext/hr/doctype/grievance_type/test_grievance_type.py new file mode 100644 index 00000000000..a02a34d41fa --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/test_grievance_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestGrievanceType(unittest.TestCase): + pass diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_list.js b/erpnext/hr/doctype/job_applicant/job_applicant_list.js index 3b9141ba79c..2ad0d591d8c 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant_list.js +++ b/erpnext/hr/doctype/job_applicant/job_applicant_list.js @@ -2,7 +2,7 @@ // MIT License. See license.txt frappe.listview_settings['Job Applicant'] = { - add_fields: ["company", "designation", "job_applicant", "status"], + add_fields: ["status"], get_indicator: function (doc) { if (doc.status == "Accepted") { return [__(doc.status), "green", "status,=," + doc.status]; diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index ae02c512c23..3a6539ece9e 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -110,6 +110,7 @@ "label": "Allocation" }, { + "allow_on_submit": 1, "bold": 1, "fieldname": "new_leaves_allocated", "fieldtype": "Float", @@ -235,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-14 15:28:26.335104", + "modified": "2021-06-03 15:28:26.335104", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", @@ -277,4 +278,4 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "employee" -} \ No newline at end of file +} diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 11302cad758..4757cd3b192 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.model.document import Document from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry +from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period class OverlapError(frappe.ValidationError): pass class BackDatedAllocationError(frappe.ValidationError): pass @@ -55,6 +56,43 @@ class LeaveAllocation(Document): if self.carry_forward: self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) + def on_update_after_submit(self): + if self.has_value_changed("new_leaves_allocated"): + self.validate_against_leave_applications() + leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count() + args = { + "leaves": leaves_to_be_added, + "from_date": self.from_date, + "to_date": self.to_date, + "is_carry_forward": 0 + } + create_leave_ledger_entry(self, args, True) + + def get_existing_leave_count(self): + ledger_entries = frappe.get_all("Leave Ledger Entry", + filters={ + "transaction_type": "Leave Allocation", + "transaction_name": self.name, + "employee": self.employee, + "company": self.company, + "leave_type": self.leave_type + }, + pluck="leaves") + total_existing_leaves = 0 + for entry in ledger_entries: + total_existing_leaves += entry + + return total_existing_leaves + + def validate_against_leave_applications(self): + leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, + self.from_date, self.to_date) + if flt(leaves_taken) > flt(self.total_leaves_allocated): + if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): + frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) + else: + frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError) + def update_leave_policy_assignments_when_no_allocations_left(self): allocations = frappe.db.get_list("Leave Allocation", filters = { "docstatus": 1, @@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date): def validate_carry_forward(leave_type): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): - frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) \ No newline at end of file + frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 6e7ae87d08c..bff06e6a91c 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import frappe +import erpnext import unittest from frappe.utils import nowdate, add_months, getdate, add_days from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type @@ -164,6 +165,51 @@ class TestLeaveAllocation(unittest.TestCase): leave_allocation.cancel() self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) + def test_leave_addition_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 40 + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 40) + + def test_leave_subtraction_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 10 + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 10) + + def test_against_leave_application_validation_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + leave_application = frappe.get_doc({ + "doctype": 'Leave Application', + "employee": employee.name, + "leave_type": "_Test Leave Type", + "from_date": add_months(nowdate(), 2), + "to_date": add_months(add_days(nowdate(), 10), 2), + "company": erpnext.get_default_company() or "_Test Company", + "docstatus": 1, + "status": "Approved", + "leave_approver": 'test@example.com' + }) + leave_application.submit() + leave_allocation.new_leaves_allocated = 8 + leave_allocation.total_leaves_allocated = 8 + self.assertRaises(frappe.ValidationError, leave_allocation.submit) + def create_leave_allocation(**args): args = frappe._dict(args) diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 533149a8235..e6c783aca22 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -41,7 +41,7 @@ class StaffingPlan(Document): detail.total_estimated_cost = 0 if detail.number_of_positions > 0: - if detail.vacancies > 0 and detail.estimated_cost_per_position: + if detail.vacancies and detail.estimated_cost_per_position: detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position) self.total_estimated_budget += detail.total_estimated_cost @@ -76,12 +76,12 @@ class StaffingPlan(Document): if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \ flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost): frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \ - for {2} as per staffing plan {3} for parent company {4}." - .format(cint(parent_plan_details[0].vacancies), + for {2} as per staffing plan {3} for parent company {4}.").format( + cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_plan_details[0].name, - parent_company)), ParentCompanyError) + parent_company), ParentCompanyError) #Get vacanices already planned for all companies down the hierarchy of Parent Company lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) @@ -98,14 +98,14 @@ class StaffingPlan(Document): (flt(parent_plan_details[0].total_estimated_cost) < \ (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))): frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ - You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}." - .format(cint(all_sibling_details.vacancies), + You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format( + cint(all_sibling_details.vacancies), all_sibling_details.total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_company, cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, - parent_plan_details[0].name))) + parent_plan_details[0].name)) def validate_with_subsidiary_plans(self, staffing_plan_detail): #Valdate this plan with all child company plan @@ -121,11 +121,11 @@ class StaffingPlan(Document): cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \ flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ - Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies" - .format(self.company, + Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format( + self.company, cint(children_details.vacancies), children_details.total_estimated_cost, - frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError) + frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError) @frappe.whitelist() def get_designation_counts(designation, company): @@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now designation, from_date, to_date) # Only a single staffing plan can be active for a designation on given date - return staffing_plan if staffing_plan else None \ No newline at end of file + return staffing_plan if staffing_plan else None diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js index b7d34b178a0..064dfb24557 100644 --- a/erpnext/hr/doctype/training_event/training_event.js +++ b/erpnext/hr/doctype/training_event/training_event.js @@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', { frappe.set_route("List", "Training Feedback"); }); } - } -}); + frm.events.set_employee_query(frm); + }, -frappe.ui.form.on("Training Event Employee", { - employee: function (frm) { + set_employee_query: function(frm) { let emp = []; for (let d in frm.doc.employees) { if (frm.doc.employees[d].employee) { @@ -40,3 +39,10 @@ frappe.ui.form.on("Training Event Employee", { }); } }); + +frappe.ui.form.on("Training Event Employee", { + employee: function(frm) { + frm.events.set_employee_query(frm); + } +}); + diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json index 2d313e9faca..bcb7d5e5bc0 100644 --- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json +++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json @@ -19,6 +19,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Employee", + "no_copy": 1, "options": "Employee" }, { @@ -68,7 +69,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-21 12:41:59.336237", + "modified": "2021-07-02 17:20:27.630176", "modified_by": "Administrator", "module": "HR", "name": "Training Event Employee", diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 4dd4570e8ca..b8953b3eaac 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): is_carry_forward, is_expired FROM `tabLeave Ledger Entry` WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 AND leaves>0 + AND docstatus=1 AND (from_date between %(from_date)s AND %(to_date)s OR to_date between %(from_date)s AND %(to_date)s OR (from_date < %(from_date)s AND to_date > %(to_date)s)) diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index c5201c22c9b..4500ba4560c 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -153,6 +153,24 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Grievance Type", + "link_to": "Grievance Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grievance", + "link_to": "Employee Grievance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Employee", "hidden": 0, @@ -823,7 +841,7 @@ "type": "Link" } ], - "modified": "2021-04-26 13:36:15.413819", + "modified": "2021-05-13 17:19:40.524444", "modified_by": "Administrator", "module": "HR", "name": "HR", diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 69d11a8653e..ff7fbbdf49a 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -60,8 +60,9 @@ class Loan(AccountsController): self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) def check_sanctioned_amount_limit(self): - total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + if sanctioned_amount_limit: + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) @@ -155,9 +156,29 @@ def update_total_amount_paid(doc): frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) def get_total_loan_amount(applicant_type, applicant, company): - return frappe.db.get_value('Loan', - {'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1}, - 'sum(loan_amount)') + pending_amount = 0 + loan_details = frappe.db.get_all("Loan", + filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1, + "status": ("!=", "Closed")}, + fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid", + "written_off_amount"]) + + interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type, + "company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)")) + + for loan in loan_details: + if loan.status in ("Disbursed", "Loan Closure Requested"): + pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + elif loan.status == "Partially Disbursed": + pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + elif loan.status == "Sanctioned": + pending_amount += flt(loan.total_payment) + + pending_amount += interest_amount + + return pending_amount def get_sanctioned_amount_limit(applicant_type, applicant, company): return frappe.db.get_value('Sanctioned Loan Amount', diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index fa4707ce2b4..314f58dd15e 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -49,7 +49,11 @@ class TestLoan(unittest.TestCase): if not frappe.db.exists("Customer", "_Test Loan Customer"): frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) - self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + if not frappe.db.exists("Customer", "_Test Loan Customer 1"): + frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True) + + self.applicant2 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") + self.applicant3 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer 1"}, "name") create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) @@ -125,6 +129,38 @@ class TestLoan(unittest.TestCase): self.assertTrue(gl_entries1) self.assertTrue(gl_entries2) + def test_sanctioned_amount_limit(self): + # Clear loan docs before checking + frappe.db.sql("DELETE FROM `tabLoan` where applicant = '_Test Loan Customer 1'") + frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'") + frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'") + + if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer", + "applicant": "_Test Loan Customer 1", "company": "_Test Company"}): + frappe.get_doc({ + "doctype": "Sanctioned Loan Amount", + "applicant_type": "Customer", + "applicant": "_Test Loan Customer 1", + "sanctioned_amount_limit": 1500000, + "company": "_Test Company" + }).insert(ignore_permissions=True) + + # Make First Loan + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge) + create_pledge(loan_application) + loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + # Make second loan greater than the sanctioned amount + loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge, + do_not_save=True) + self.assertRaises(frappe.ValidationError, loan_application.save) + def test_regular_loan_repayment(self): pledge = [{ "loan_security": "Test Security 1", @@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase): unpledge_request.load_from_db() self.assertEqual(unpledge_request.docstatus, 1) - def test_santined_loan_security_unpledge(self): + def test_sanctioned_loan_security_unpledge(self): pledge = [{ "loan_security": "Test Security 1", "qty": 4000.00 @@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount): return lr def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None, - repayment_periods=None, posting_date=None): + repayment_periods=None, posting_date=None, do_not_save=False): loan_application = frappe.new_doc('Loan Application') loan_application.applicant_type = 'Customer' loan_application.company = company @@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep for pledge in proposed_pledges: loan_application.append('proposed_pledges', pledge) + if do_not_save: + return loan_application + loan_application.save() loan_application.submit() diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index 9c0147e55ba..d8f3577b2c3 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -46,9 +46,11 @@ class LoanApplication(Document): frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount)) def check_sanctioned_amount_limit(self): - total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + if sanctioned_amount_limit: + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 3d99b1f3040..b8b1a40b5fd 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -235,70 +235,71 @@ class LoanRepayment(AccountsController): else: remarks = _("Repayment against Loan: ") + self.against_loan - if self.total_penalty_paid: + if not loan_details.repay_from_salary: + if self.total_penalty_paid: + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "against": loan_details.payment_account, + "debit": self.total_penalty_paid, + "debit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.penalty_income_account, + "against": loan_details.payment_account, + "credit": self.total_penalty_paid, + "credit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + gle_map.append( self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.payment_account, - "debit": self.total_penalty_paid, - "debit_in_account_currency": self.total_penalty_paid, + "account": loan_details.payment_account, + "against": loan_details.loan_account + ", " + loan_details.interest_income_account + + ", " + loan_details.penalty_income_account, + "debit": self.amount_paid, + "debit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": getdate(self.posting_date) }) ) gle_map.append( self.get_gl_dict({ - "account": loan_details.penalty_income_account, + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, "against": loan_details.payment_account, - "credit": self.total_penalty_paid, - "credit_in_account_currency": self.total_penalty_paid, + "credit": self.amount_paid, + "credit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) ) - gle_map.append( - self.get_gl_dict({ - "account": loan_details.payment_account, - "against": loan_details.loan_account + ", " + loan_details.interest_income_account - + ", " + loan_details.penalty_income_account, - "debit": self.amount_paid, - "debit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, - "against": loan_details.payment_account, - "credit": self.amount_paid, - "credit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) - - if gle_map: - make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) def create_repayment_entry(loan, applicant, company, posting_date, loan_type, payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None): diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json index 18bd4aea78b..68bac8ed8cd 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json @@ -35,7 +35,9 @@ "no_copy": 1, "options": "Loan Security Pledge", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "loan_application.applicant", @@ -45,47 +47,63 @@ "in_standard_filter": 1, "label": "Applicant", "options": "applicant_type", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "loan_security_details_section", "fieldtype": "Section Break", - "label": "Loan Security Details" + "label": "Loan Security Details", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_3", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "loan", "fieldtype": "Link", "label": "Loan", - "options": "Loan" + "options": "Loan", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "loan_application", "fieldtype": "Link", "label": "Loan Application", - "options": "Loan Application" + "options": "Loan Application", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_security_value", "fieldtype": "Currency", "label": "Total Security Value", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "maximum_loan_value", "fieldtype": "Currency", "label": "Maximum Loan Value", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "loan_details_section", "fieldtype": "Section Break", - "label": "Loan Details" + "label": "Loan Details", + "show_days": 1, + "show_seconds": 1 }, { "default": "Requested", @@ -94,37 +112,49 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "Requested\nUnpledged\nPledged\nPartially Pledged", - "read_only": 1 + "options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pledge_time", "fieldtype": "Datetime", "label": "Pledge Time", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "securities", "fieldtype": "Table", "label": "Securities", "options": "Pledge", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_11", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_10", "fieldtype": "Section Break", - "label": "Totals" + "label": "Totals", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "loan.applicant_type", @@ -132,35 +162,45 @@ "fieldtype": "Select", "label": "Applicant Type", "options": "Employee\nMember\nCustomer", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "more_information_section", "fieldtype": "Section Break", - "label": "More Information" + "label": "More Information", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "reference_no", "fieldtype": "Data", - "label": "Reference No" + "label": "Reference No", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_18", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "description", "fieldtype": "Text", - "label": "Description" + "label": "Description", + "show_days": 1, + "show_seconds": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 18:23:16.953305", + "modified": "2021-06-29 17:15:16.082256", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Pledge", diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index cbc8376aa56..c390b6c526d 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -23,6 +23,12 @@ class LoanSecurityPledge(Document): update_shortfall_status(self.loan, self.total_security_value) update_loan(self.loan, self.maximum_loan_value) + def on_cancel(self): + if self.loan: + self.db_set("status", "Cancelled") + self.db_set("pledge_time", None) + update_loan(self.loan, self.maximum_loan_value, cancel=1) + def validate_duplicate_securities(self): security_list = [] for security in self.securities: @@ -36,7 +42,7 @@ class LoanSecurityPledge(Document): existing_pledge = '' if self.loan: - existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan}, ['name']) + existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name']) if existing_pledge: loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type']) @@ -77,8 +83,12 @@ class LoanSecurityPledge(Document): self.total_security_value = total_security_value self.maximum_loan_value = maximum_loan_value -def update_loan(loan, maximum_value_against_pledge): +def update_loan(loan, maximum_value_against_pledge, cancel=0): maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount']) - frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 - WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) + if cancel: + frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s + WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan)) + else: + frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 + WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js index 4c31bd0b7d0..f19a1b08681 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js @@ -13,7 +13,7 @@ frappe.ui.form.on('Blanket Order', { refresh: function(frm) { erpnext.hide_company(); - if (frm.doc.customer && frm.doc.docstatus === 1) { + if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) { frm.add_custom_button(__("Sales Order"), function() { frappe.model.open_mapped_doc({ method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json index 0330e5c85c9..a63fc4da69a 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2018-05-24 07:18:08.256060", "doctype": "DocType", @@ -79,6 +80,7 @@ "reqd": 1 }, { + "allow_on_submit": 1, "fieldname": "to_date", "fieldtype": "Date", "label": "To Date", @@ -129,8 +131,10 @@ "label": "Terms and Conditions Details" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-11-18 19:37:37.151686", + "links": [], + "modified": "2021-06-29 00:30:30.621636", "modified_by": "Administrator", "module": "Manufacturing", "name": "Blanket Order", diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index a09a5e34300..15a7c316c91 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", { refresh: function(frm) { frm.toggle_enable("item", frm.doc.__islocal); - toggle_operations(frm); frm.set_indicator_formatter('item_code', function(doc) { @@ -326,8 +325,7 @@ frappe.ui.form.on("BOM", { freeze: true, args: { update_parent: true, - from_child_bom:false, - save: frm.doc.docstatus === 1 ? true : false + from_child_bom:false }, callback: function(r) { refresh_field("items"); @@ -651,15 +649,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) { erpnext.bom.calculate_total(frm.doc); }); -var toggle_operations = function(frm) { - frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1); - frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1); - frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1); -}; - frappe.ui.form.on("BOM", "with_operations", function(frm) { if(!cint(frm.doc.with_operations)) { frm.set_value("operations", []); } - toggle_operations(frm); }); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index f551b91597f..7e539183b0c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -36,6 +36,9 @@ "materials_section", "inspection_required", "quality_inspection_template", + "column_break_31", + "bom_level", + "section_break_33", "items", "scrap_section", "scrap_items", @@ -193,6 +196,7 @@ }, { "default": "Work Order", + "depends_on": "with_operations", "fieldname": "transfer_material_against", "fieldtype": "Select", "label": "Transfer Material Against", @@ -235,6 +239,7 @@ { "fieldname": "operations_section", "fieldtype": "Section Break", + "hide_border": 1, "oldfieldtype": "Section Break" }, { @@ -245,6 +250,7 @@ "options": "Routing" }, { + "depends_on": "with_operations", "fieldname": "operations", "fieldtype": "Table", "label": "Operations", @@ -510,6 +516,22 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "column_break_31", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "bom_level", + "fieldtype": "Int", + "label": "BOM Level", + "read_only": 1 + }, + { + "fieldname": "section_break_33", + "fieldtype": "Section Break", + "hide_border": 1 } ], "icon": "fa fa-sitemap", @@ -517,7 +539,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-05-21 12:29:32.634952", + "modified": "2021-05-16 12:25:09.081968", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index d1f63854c71..c32a8a95a17 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1,7 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from typing import List +from collections import deque import frappe, erpnext from frappe.utils import cint, cstr, flt, today from frappe import _ @@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc import functools -from six import string_types - from operator import itemgetter form_grid_templates = { "items": "templates/form_grid/item_grid.html" } + +class BOMTree: + """Full tree representation of a BOM""" + + # specifying the attributes to save resources + # ref: https://docs.python.org/3/reference/datamodel.html#slots + __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] + + def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None: + self.name = name # name of node, BOM number if is_bom else item_code + self.child_items: List["BOMTree"] = [] # list of child items + self.is_bom = is_bom # true if the node is a BOM and not a leaf item + self.item_code: str = None # item_code associated with node + self.qty = qty # required unit quantity to make one unit of parent item. + self.exploded_qty = exploded_qty # total exploded qty required for making root of tree. + if not self.is_bom: + self.item_code = self.name + else: + self.__create_tree() + + def __create_tree(self): + bom = frappe.get_cached_doc("BOM", self.name) + self.item_code = bom.item + + for item in bom.get("items", []): + qty = item.qty / bom.quantity # quantity per unit + exploded_qty = self.exploded_qty * qty + if item.bom_no: + child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) + self.child_items.append(child) + else: + self.child_items.append( + BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty) + ) + + def level_order_traversal(self) -> List["BOMTree"]: + """Get level order traversal of tree. + E.g. for following tree the traversal will return list of nodes in order from top to bottom. + BOM: + - SubAssy1 + - item1 + - item2 + - SubAssy2 + - item3 + - item4 + + returns = [SubAssy1, item1, item2, SubAssy2, item3, item4] + """ + traversal = [] + q = deque() + q.append(self) + + while q: + node = q.popleft() + + for child in node.child_items: + traversal.append(child) + q.append(child) + + return traversal + + def __str__(self) -> str: + return ( + f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}" + f" exploded_qty: {self.exploded_qty}" + ) + + def __repr__(self, level: int = 0) -> str: + rep = "┃ " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n" + for child in self.child_items: + rep += child.__repr__(level=level + 1) + return rep + class BOM(WebsiteGenerator): website = frappe._dict( # page_title_field = "item_name", @@ -81,7 +153,8 @@ class BOM(WebsiteGenerator): self.validate_operations() self.calculate_cost() self.update_stock_qty() - self.update_cost(update_parent=False, from_child_bom=True, save=False) + self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) + self.set_bom_level() def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -152,7 +225,7 @@ class BOM(WebsiteGenerator): if not args: args = frappe.form_dict.get('args') - if isinstance(args, string_types): + if isinstance(args, str): import json args = json.loads(args) @@ -213,7 +286,7 @@ class BOM(WebsiteGenerator): return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) @frappe.whitelist() - def update_cost(self, update_parent=True, from_child_bom=False, save=True): + def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True): if self.docstatus == 2: return @@ -242,7 +315,7 @@ class BOM(WebsiteGenerator): if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True - self.calculate_cost() + self.calculate_cost(update_hour_rate) if save: self.db_update() @@ -403,32 +476,47 @@ class BOM(WebsiteGenerator): bom_list.reverse() return bom_list - def calculate_cost(self): + def calculate_cost(self, update_hour_rate = False): """Calculate bom totals""" - self.calculate_op_cost() + self.calculate_op_cost(update_hour_rate) self.calculate_rm_cost() self.calculate_sm_cost() self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost - def calculate_op_cost(self): + def calculate_op_cost(self, update_hour_rate = False): """Update workstation rate and calculates totals""" self.operating_cost = 0 self.base_operating_cost = 0 for d in self.get('operations'): if d.workstation: - if not d.hour_rate: - hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate")) - d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate - - if d.hour_rate and d.time_in_mins: - d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate) - d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0 - d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate) + self.update_rate_and_time(d, update_hour_rate) self.operating_cost += flt(d.operating_cost) self.base_operating_cost += flt(d.base_operating_cost) + def update_rate_and_time(self, row, update_hour_rate = False): + if not row.hour_rate or update_hour_rate: + hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate")) + row.hour_rate = (hour_rate / flt(self.conversion_rate) + if self.conversion_rate and hour_rate else hour_rate) + + if self.routing: + row.time_in_mins = flt(frappe.db.get_value("BOM Operation", { + "workstation": row.workstation, + "operation": row.operation, + "sequence_id": row.sequence_id, + "parent": self.routing + }, ["time_in_mins"])) + + if row.hour_rate and row.time_in_mins: + row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) + row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 + row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) + + if update_hour_rate: + row.db_update() + def calculate_rm_cost(self): """Fetch RM rate as per today's valuation rate and calculate totals""" total_rm_cost = 0 @@ -575,7 +663,7 @@ class BOM(WebsiteGenerator): self.get_routing() def validate_operations(self): - if self.with_operations and not self.get('operations'): + if self.with_operations and not self.get('operations') and self.docstatus == 1: frappe.throw(_("Operations cannot be left blank")) if self.with_operations: @@ -585,6 +673,24 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + def get_tree_representation(self) -> BOMTree: + """Get a complete tree representation preserving order of child items.""" + return BOMTree(self.name) + + def set_bom_level(self, update=False): + levels = [] + + self.bom_level = 0 + for row in self.items: + if row.bom_no: + levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0) + + if levels: + self.bom_level = max(levels) + 1 + + if update: + self.db_set("bom_level", self.bom_level) + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) @@ -768,7 +874,7 @@ def get_children(doctype, parent=None, is_root=False, **filters): frappe.form_dict.parent = parent if frappe.form_dict.parent: - bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent) + bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent) frappe.has_permission("BOM", doc=bom_doc, throw=True) bom_items = frappe.get_all('BOM Item', @@ -779,7 +885,7 @@ def get_children(doctype, parent=None, is_root=False, **filters): item_names = tuple(d.get('item_code') for d in bom_items) items = frappe.get_list('Item', - fields=['image', 'description', 'name', 'stock_uom', 'item_name'], + fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'], filters=[['name', 'in', item_names]]) # to get only required item dicts for bom_item in bom_items: @@ -792,6 +898,7 @@ def get_children(doctype, parent=None, is_root=False, **filters): bom_item.parent_bom_qty = bom_doc.quantity bom_item.expandable = 0 if bom_item.value in ('', None) else 1 + bom_item.image = frappe.db.escape(bom_item.image) return bom_items @@ -975,7 +1082,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if filters and filters.get("is_stock_item"): query_filters["is_stock_item"] = 1 - + return frappe.get_all("Item", fields = fields, filters=query_filters, or_filters = or_cond_filters, order_by=order_by, @@ -1008,6 +1115,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): }, 'BOM Item': { 'doctype': 'BOM Item', + # stop get_mapped_doc copying parent bom_no to children + 'field_no_map': ['bom_no'], 'condition': lambda doc: doc.has_variants == 0 }, }, target_doc, postprocess) diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html index 6cd5f8cb3cf..6088e46265b 100644 --- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html +++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html @@ -1,13 +1,31 @@
+ {% if data.value %} + + {{ __("Open BOM {0}", [data.value.bold()]) }} + {% endif %} + {% if data.item_code %} + + {{ __("Open Item {0}", [data.item_code.bold()]) }} + {% endif %} +
+
diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js
index 185b9ed4bcf..60fb377f476 100644
--- a/erpnext/manufacturing/doctype/bom/bom_tree.js
+++ b/erpnext/manufacturing/doctype/bom/bom_tree.js
@@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = {
if(node.is_root && node.data.value!="BOM") {
frappe.model.with_doc("BOM", node.data.value, function() {
var bom = frappe.model.get_doc("BOM", node.data.value);
- node.data.image = bom.image || "";
+ node.data.image = escape(bom.image) || "";
node.data.description = bom.description || "";
});
}
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index e1cca9e3ef4..57a54587269 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -2,16 +2,16 @@
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
+from collections import deque
import unittest
import frappe
from frappe.utils import cstr, flt
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
-from six import string_types
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+from erpnext.tests.test_subcontracting import set_backflush_based_on
test_records = frappe.get_test_records('BOM')
@@ -122,7 +122,7 @@ class TestBOM(unittest.TestCase):
bom.items[0].conversion_factor = 5
bom.insert()
- bom.update_cost()
+ bom.update_cost(update_hour_rate = False)
# test amounts in selected currency
self.assertEqual(bom.items[0].rate, 300)
@@ -160,6 +160,7 @@ class TestBOM(unittest.TestCase):
def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1"
+ set_backflush_based_on('Material Transferred for Subcontract')
if not frappe.db.exists('Item', item_code):
make_item(item_code, {
@@ -225,11 +226,88 @@ class TestBOM(unittest.TestCase):
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEqual(bom_items, supplied_items)
+ def test_bom_tree_representation(self):
+ bom_tree = {
+ "Assembly": {
+ "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
+ "SubAssembly2": {"ChildPart3": {}},
+ "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
+ "ChildPart5": {},
+ "ChildPart6": {},
+ "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
+ }
+ }
+ parent_bom = create_nested_bom(bom_tree, prefix="")
+ created_tree = parent_bom.get_tree_representation()
+
+ reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
+ created_order = created_tree.level_order_traversal()
+
+ self.assertEqual(len(reqd_order), len(created_order))
+
+ for reqd_item, created_item in zip(reqd_order, created_order):
+ self.assertEqual(reqd_item, created_item.item_code)
+
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
+
+
+
+def level_order_traversal(node):
+ traversal = []
+ q = deque()
+ q.append(node)
+
+ while q:
+ node = q.popleft()
+
+ for node_name, subtree in node.items():
+ traversal.append(node_name)
+ q.append(subtree)
+
+ return traversal
+
+def create_nested_bom(tree, prefix="_Test bom "):
+ """ Helper function to create a simple nested bom from tree describing item names. (along with required items)
+ """
+
+ def create_items(bom_tree):
+ for item_code, subtree in bom_tree.items():
+ bom_item_code = prefix + item_code
+ if not frappe.db.exists("Item", bom_item_code):
+ frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert()
+ create_items(subtree)
+ create_items(tree)
+
+ def dfs(tree, node):
+ """naive implementation for searching right subtree"""
+ for node_name, subtree in tree.items():
+ if node_name == node:
+ return subtree
+ else:
+ result = dfs(subtree, node)
+ if result is not None:
+ return result
+
+ order_of_creating_bom = reversed(level_order_traversal(tree))
+
+ for item in order_of_creating_bom:
+ child_items = dfs(tree, item)
+ if child_items:
+ bom_item_code = prefix + item
+ bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
+ for child_item in child_items.keys():
+ bom.append("items", {"item_code": prefix + child_item})
+ bom.insert()
+ bom.submit()
+
+ return bom # parent bom is last bom
+
+
def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None):
- if warehouse_list and isinstance(warehouse_list, string_types):
+ if warehouse_list and isinstance(warehouse_list, str):
warehouse_list = [warehouse_list]
if not warehouse_list:
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index 07464e3e766..4458e6db234 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -13,10 +13,10 @@
"col_break1",
"hour_rate",
"time_in_mins",
- "batch_size",
"operating_cost",
"base_hour_rate",
"base_operating_cost",
+ "batch_size",
"image"
],
"fields": [
@@ -61,6 +61,8 @@
},
{
"description": "In minutes",
+ "fetch_from": "operation.total_operation_time",
+ "fetch_if_empty": 1,
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
@@ -104,7 +106,8 @@
"label": "Image"
},
{
- "default": "1",
+ "fetch_from": "operation.batch_size",
+ "fetch_if_empty": 1,
"fieldname": "batch_size",
"fieldtype": "Int",
"label": "Batch Size"
@@ -120,7 +123,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-13 18:14:10.018774",
+ "modified": "2021-01-12 14:48:09.596843",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 4e8dd41022b..81860c9fbcf 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', {
}
};
});
+
+ frm.set_indicator_formatter('sub_operation',
+ function(doc) {
+ if (doc.status == "Pending") {
+ return "red";
+ } else {
+ return doc.status === "Complete" ? "green" : "orange";
+ }
+ }
+ );
},
refresh: function(frm) {
@@ -31,6 +41,10 @@ frappe.ui.form.on('Job Card', {
}
}
+ if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
+ frm.trigger('setup_corrective_job_card');
+ }
+
frm.set_query("quality_inspection", function() {
return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
@@ -43,12 +57,62 @@ frappe.ui.form.on('Job Card', {
frm.trigger("toggle_operation_number");
- if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
+ if (frm.doc.docstatus == 0 && !frm.is_new() &&
+ (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons");
}
},
+ setup_corrective_job_card: function(frm) {
+ frm.add_custom_button(__('Corrective Job Card'), () => {
+ let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation);
+
+ let fields = [
+ {
+ fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation',
+ fieldname: 'operation', get_query() {
+ return {
+ filters: {
+ "is_corrective_operation": 1
+ }
+ };
+ }
+ }, {
+ fieldtype: 'Link', label: __('For Operation'), options: 'Operation',
+ fieldname: 'for_operation', get_query() {
+ return {
+ filters: {
+ "name": ["in", operations]
+ }
+ };
+ }
+ }
+ ];
+
+ frappe.prompt(fields, d => {
+ frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
+ }, __("Select Corrective Operation"));
+ }, __('Make'));
+ },
+
+ make_corrective_job_card: function(frm, operation, for_operation) {
+ frappe.call({
+ method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card',
+ args: {
+ source_name: frm.doc.name,
+ operation: operation,
+ for_operation: for_operation
+ },
+ callback: function(r) {
+ if (r.message) {
+ frappe.model.sync(r.message);
+ frappe.set_route("Form", r.message.doctype, r.message.name);
+ }
+ }
+ });
+ },
+
operation: function(frm) {
frm.trigger("toggle_operation_number");
@@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', {
prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard");
- if (!frm.doc.job_started) {
- frm.add_custom_button(__("Start"), () => {
- if (!frm.doc.employee) {
- frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee",
- fieldname: 'employee'}, d => {
- if (d.employee) {
- frm.set_value("employee", d.employee);
- } else {
- frm.events.start_job(frm);
- }
- }, __("Enter Value"), __("Start"));
+
+ if (!frm.doc.started_time && !frm.doc.current_time) {
+ frm.add_custom_button(__("Start Job"), () => {
+ if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
+ frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
+ options: "Job Card Time Log", fieldname: 'employees'}, d => {
+ frm.events.start_job(frm, "Work In Progress", d.employees);
+ }, __("Assign Job to Employee"));
} else {
- frm.events.start_job(frm);
+ frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
}
}).addClass("btn-primary");
} else if (frm.doc.status == "On Hold") {
- frm.add_custom_button(__("Resume"), () => {
- frappe.flags.resume_job = 1;
- frm.events.start_job(frm);
+ frm.add_custom_button(__("Resume Job"), () => {
+ frm.events.start_job(frm, "Resume Job", frm.doc.employee);
}).addClass("btn-primary");
} else {
- frm.add_custom_button(__("Pause"), () => {
- frappe.flags.pause_job = 1;
- frm.set_value("status", "On Hold");
- frm.events.complete_job(frm);
+ frm.add_custom_button(__("Pause Job"), () => {
+ frm.events.complete_job(frm, "On Hold");
});
- frm.add_custom_button(__("Complete"), () => {
- let completed_time = frappe.datetime.now_datetime();
- frm.trigger("hide_timer");
+ frm.add_custom_button(__("Complete Job"), () => {
+ var sub_operations = frm.doc.sub_operations;
- if (frm.doc.for_quantity) {
+ let set_qty = true;
+ if (sub_operations && sub_operations.length > 1) {
+ set_qty = false;
+ let last_op_row = sub_operations[sub_operations.length - 2];
+
+ if (last_op_row.status == 'Complete') {
+ set_qty = true;
+ }
+ }
+
+ if (set_qty) {
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
- fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => {
- frm.events.complete_job(frm, completed_time, data.qty);
- }, __("Enter Value"), __("Complete"));
+ fieldname: 'qty', default: frm.doc.for_quantity}, data => {
+ frm.events.complete_job(frm, "Complete", data.qty);
+ }, __("Enter Value"));
} else {
- frm.events.complete_job(frm, completed_time, 0);
+ frm.events.complete_job(frm, "Complete", 0.0);
}
}).addClass("btn-primary");
}
},
- start_job: function(frm) {
- let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs');
- row.from_time = frappe.datetime.now_datetime();
- frm.set_value('job_started', 1);
- frm.set_value('started_time' , row.from_time);
- frm.set_value("status", "Work In Progress");
-
- if (!frappe.flags.resume_job) {
- frm.set_value('current_time' , 0);
- }
-
- frm.save();
+ start_job: function(frm, status, employee) {
+ const args = {
+ job_card_id: frm.doc.name,
+ start_time: frappe.datetime.now_datetime(),
+ employees: employee,
+ status: status
+ };
+ frm.events.make_time_log(frm, args);
},
- complete_job: function(frm, completed_time, completed_qty) {
- frm.doc.time_logs.forEach(d => {
- if (d.from_time && !d.to_time) {
- d.to_time = completed_time || frappe.datetime.now_datetime();
- d.completed_qty = completed_qty || 0;
+ complete_job: function(frm, status, completed_qty) {
+ const args = {
+ job_card_id: frm.doc.name,
+ complete_time: frappe.datetime.now_datetime(),
+ status: status,
+ completed_qty: completed_qty
+ };
+ frm.events.make_time_log(frm, args);
+ },
- if(frappe.flags.pause_job) {
- let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0;
- frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0));
- } else {
- frm.set_value('started_time' , '');
- frm.set_value('job_started', 0);
- frm.set_value('current_time' , 0);
- }
+ make_time_log: function(frm, args) {
+ frm.events.update_sub_operation(frm, args);
- frm.save();
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
+ args: {
+ args: args
+ },
+ freeze: true,
+ callback: function () {
+ frm.reload_doc();
+ frm.trigger("make_dashboard");
}
});
},
+ update_sub_operation: function(frm, args) {
+ if (frm.doc.sub_operations && frm.doc.sub_operations.length) {
+ let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete');
+ if (sub_operations && sub_operations.length) {
+ args["sub_operation"] = sub_operations[0].sub_operation;
+ }
+ }
+ },
+
validate: function(frm) {
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
frm.trigger("reset_timer");
}
},
- employee: function(frm) {
- if (frm.doc.job_started && !frm.doc.current_time) {
- frm.trigger("reset_timer");
- } else {
- frm.events.start_job(frm);
- }
- },
-
reset_timer: function(frm) {
frm.set_value('started_time' , '');
- frm.set_value('job_started', 0);
- frm.set_value('current_time' , 0);
},
make_dashboard: function(frm) {
@@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', {
},
to_time: function(frm) {
- frm.set_value('job_started', 0);
frm.set_value('started_time', '');
}
})
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 5713f697e99..046e2fd1825 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -9,38 +9,49 @@
"naming_series",
"work_order",
"bom_no",
- "workstation",
- "operation",
- "operation_row_number",
"column_break_4",
"posting_date",
"company",
- "remarks",
"production_section",
"production_item",
"item_name",
"for_quantity",
- "quality_inspection",
- "wip_warehouse",
+ "serial_no",
"column_break_12",
- "employee",
- "employee_name",
- "status",
+ "wip_warehouse",
+ "quality_inspection",
"project",
+ "batch_no",
+ "operation_section_section",
+ "operation",
+ "operation_row_number",
+ "column_break_18",
+ "workstation",
+ "employee",
+ "section_break_21",
+ "sub_operations",
"timing_detail",
"time_logs",
"section_break_13",
"total_completed_qty",
- "total_time_in_mins",
"column_break_15",
+ "total_time_in_mins",
"section_break_8",
"items",
+ "corrective_operation_section",
+ "for_job_card",
+ "is_corrective_job_card",
+ "column_break_33",
+ "hour_rate",
+ "for_operation",
"more_information",
"operation_id",
"sequence_id",
"transferred_qty",
"requested_qty",
+ "status",
"column_break_20",
+ "remarks",
"barcode",
"job_started",
"started_time",
@@ -117,13 +128,6 @@
"fieldtype": "Section Break",
"label": "Timing Detail"
},
- {
- "fieldname": "employee",
- "fieldtype": "Link",
- "in_standard_filter": 1,
- "label": "Employee",
- "options": "Employee"
- },
{
"allow_bulk_edit": 1,
"fieldname": "time_logs",
@@ -133,9 +137,11 @@
},
{
"fieldname": "section_break_13",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "hide_border": 1
},
{
+ "default": "0",
"fieldname": "total_completed_qty",
"fieldtype": "Float",
"label": "Total Completed Qty",
@@ -160,8 +166,7 @@
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
- "options": "Job Card Item",
- "read_only": 1
+ "options": "Job Card Item"
},
{
"collapsible": 1,
@@ -251,12 +256,7 @@
"reqd": 1
},
{
- "fetch_from": "employee.employee_name",
- "fieldname": "employee_name",
- "fieldtype": "Read Only",
- "label": "Employee Name"
- },
- {
+ "collapsible": 1,
"fieldname": "production_section",
"fieldtype": "Section Break",
"label": "Production"
@@ -314,11 +314,89 @@
"label": "Quality Inspection",
"no_copy": 1,
"options": "Quality Inspection"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "sub_operations",
+ "fieldtype": "Table",
+ "label": "Sub Operations",
+ "options": "Job Card Operation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "operation_section_section",
+ "fieldtype": "Section Break",
+ "label": "Operation Section"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_21",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "depends_on": "is_corrective_job_card",
+ "fieldname": "hour_rate",
+ "fieldtype": "Currency",
+ "label": "Hour Rate"
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "is_corrective_job_card",
+ "fieldname": "corrective_operation_section",
+ "fieldtype": "Section Break",
+ "label": "Corrective Operation"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_corrective_job_card",
+ "fieldtype": "Check",
+ "label": "Is Corrective Job Card",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_33",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "for_job_card",
+ "fieldtype": "Link",
+ "label": "For Job Card",
+ "options": "Job Card",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "for_job_card.operation",
+ "fetch_if_empty": 1,
+ "fieldname": "for_operation",
+ "fieldtype": "Link",
+ "label": "For Operation",
+ "options": "Operation"
+ },
+ {
+ "fieldname": "employee",
+ "fieldtype": "Table MultiSelect",
+ "label": "Employee",
+ "options": "Job Card Time Log"
+ },
+ {
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "label": "Serial No"
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch No",
+ "options": "Batch"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-19 18:26:50.531664",
+ "modified": "2021-03-16 15:59:32.766484",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index cdc45188942..420bb008039 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -5,11 +5,12 @@
from __future__ import unicode_literals
import frappe
import datetime
+import json
from frappe import _, bold
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
- get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form)
+ get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
@@ -25,10 +26,21 @@ class JobCard(Document):
self.set_status()
self.validate_operation_id()
self.validate_sequence_id()
+ self.get_sub_operations()
+ self.update_sub_operation_status()
+
+ def get_sub_operations(self):
+ if self.operation:
+ self.sub_operations = []
+ for row in frappe.get_all("Sub Operation",
+ filters = {"parent": self.operation}, fields=["operation", "idx"]):
+ row.status = "Pending"
+ row.sub_operation = row.operation
+ self.append("sub_operations", row)
def validate_time_logs(self):
- self.total_completed_qty = 0.0
self.total_time_in_mins = 0.0
+ self.total_completed_qty = 0.0
if self.get('time_logs'):
for d in self.get('time_logs'):
@@ -44,11 +56,14 @@ class JobCard(Document):
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
self.total_time_in_mins += d.time_in_mins
- if d.completed_qty:
+ if d.completed_qty and not self.sub_operations:
self.total_completed_qty += d.completed_qty
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
+ for row in self.sub_operations:
+ self.total_completed_qty += row.completed_qty
+
def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1
@@ -57,7 +72,7 @@ class JobCard(Document):
self.workstation, 'production_capacity') or 1
validate_overlap_for = " and jc.workstation = %(workstation)s "
- if self.employee:
+ if args.get("employee"):
# override capacity for employee
production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s "
@@ -80,7 +95,7 @@ class JobCard(Document):
"to_time": args.to_time,
"name": args.name or "No Name",
"parent": args.parent or "No Name",
- "employee": self.employee,
+ "employee": args.get("employee"),
"workstation": self.workstation
}, as_dict=True)
@@ -158,6 +173,108 @@ class JobCard(Document):
row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[0].start_time))
+ def add_time_log(self, args):
+ last_row = []
+ employees = args.employees
+ if isinstance(employees, str):
+ employees = json.loads(employees)
+
+ if self.time_logs and len(self.time_logs) > 0:
+ last_row = self.time_logs[-1]
+
+ self.reset_timer_value(args)
+ if last_row and args.get("complete_time"):
+ for row in self.time_logs:
+ if not row.to_time:
+ row.update({
+ "to_time": get_datetime(args.get("complete_time")),
+ "operation": args.get("sub_operation"),
+ "completed_qty": args.get("completed_qty") or 0.0
+ })
+ elif args.get("start_time"):
+ new_args = {
+ "from_time": get_datetime(args.get("start_time")),
+ "operation": args.get("sub_operation"),
+ "completed_qty": 0.0
+ }
+
+ if employees:
+ for name in employees:
+ new_args.employee = name.get('employee')
+ self.add_start_time_log(new_args)
+ else:
+ self.add_start_time_log(new_args)
+
+ if not self.employee and employees:
+ self.set_employees(employees)
+
+ if self.status == "On Hold":
+ self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
+
+ self.save()
+
+ def add_start_time_log(self, args):
+ self.append("time_logs", args)
+
+ def set_employees(self, employees):
+ for name in employees:
+ self.append('employee', {
+ 'employee': name.get('employee'),
+ 'completed_qty': 0.0
+ })
+
+ def reset_timer_value(self, args):
+ self.started_time = None
+
+ if args.get("status") in ["Work In Progress", "Complete"]:
+ self.current_time = 0.0
+
+ if args.get("status") == "Work In Progress":
+ self.started_time = get_datetime(args.get("start_time"))
+
+ if args.get("status") == "Resume Job":
+ args["status"] = "Work In Progress"
+
+ if args.get("status"):
+ self.status = args.get("status")
+
+ def update_sub_operation_status(self):
+ if not (self.sub_operations and self.time_logs):
+ return
+
+ operation_wise_completed_time = {}
+ for time_log in self.time_logs:
+ if time_log.operation not in operation_wise_completed_time:
+ operation_wise_completed_time.setdefault(time_log.operation,
+ frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
+
+ op_row = operation_wise_completed_time[time_log.operation]
+ op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
+ if self.status == 'On Hold':
+ op_row.status = 'Pause'
+
+ op_row.employee.append(time_log.employee)
+ if time_log.time_in_mins:
+ op_row.completed_time += time_log.time_in_mins
+ op_row.completed_qty += time_log.completed_qty
+
+ for row in self.sub_operations:
+ operation_deatils = operation_wise_completed_time.get(row.sub_operation)
+ if operation_deatils:
+ if row.status != 'Complete':
+ row.status = operation_deatils.status
+
+ row.completed_time = operation_deatils.completed_time
+ if operation_deatils.employee:
+ row.completed_time = row.completed_time / len(set(operation_deatils.employee))
+
+ if operation_deatils.completed_qty:
+ row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
+ else:
+ row.status = 'Pending'
+ row.completed_time = 0.0
+ row.completed_qty = 0.0
+
def update_time_logs(self, row):
self.append("time_logs", {
"from_time": row.planned_start_time,
@@ -182,15 +299,18 @@ class JobCard(Document):
if self.get('operation') == d.operation:
self.append('items', {
- 'item_code': d.item_code,
- 'source_warehouse': d.source_warehouse,
- 'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'),
- 'item_name': d.item_name,
- 'description': d.description,
- 'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty
+ "item_code": d.item_code,
+ "source_warehouse": d.source_warehouse,
+ "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
+ "item_name": d.item_name,
+ "description": d.description,
+ "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
+ "rate": d.rate,
+ "amount": d.amount
})
def on_submit(self):
+ self.validate_transfer_qty()
self.validate_job_card()
self.update_work_order()
self.set_transferred_qty()
@@ -199,7 +319,16 @@ class JobCard(Document):
self.update_work_order()
self.set_transferred_qty()
+ def validate_transfer_qty(self):
+ if self.items and self.transferred_qty < self.for_quantity:
+ frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
+ .format(self.name))
+
def validate_job_card(self):
+ if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
+ frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
+ .format(get_link_to_form('Work Order', self.work_order)))
+
if not self.time_logs:
frappe.throw(_("Time logs are required for {0} {1}")
.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
@@ -215,6 +344,10 @@ class JobCard(Document):
if not self.work_order:
return
+ if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
+ 'add_corrective_operation_cost_in_finished_good_valuation')):
+ return
+
for_quantity, time_in_mins = 0, 0
from_time_list, to_time_list = [], []
@@ -225,10 +358,24 @@ class JobCard(Document):
time_in_mins = flt(data[0].time_in_mins)
wo = frappe.get_doc('Work Order', self.work_order)
- if self.operation_id:
+
+ if self.is_corrective_job_card:
+ self.update_corrective_in_work_order(wo)
+
+ elif self.operation_id:
self.validate_produced_quantity(for_quantity, wo)
self.update_work_order_data(for_quantity, time_in_mins, wo)
+ def update_corrective_in_work_order(self, wo):
+ wo.corrective_operation_cost = 0.0
+ for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
+ filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
+ wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
+
+ wo.calculate_operating_cost()
+ wo.flags.ignore_validate_update_after_submit = True
+ wo.save()
+
def validate_produced_quantity(self, for_quantity, wo):
if self.docstatus < 2: return
@@ -248,8 +395,8 @@ class JobCard(Document):
min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE
- jctl.parent = jc.name and jc.work_order = %s
- and jc.operation_id = %s and jc.docstatus = 1
+ jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
+ and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
""", (self.work_order, self.operation_id), as_dict=1)
for data in wo.operations:
@@ -271,7 +418,8 @@ class JobCard(Document):
def get_current_operation_data(self):
return frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
- filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
+ filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
+ "is_corrective_job_card": 0})
def set_transferred_qty_in_job_card(self, ste_doc):
for row in ste_doc.items:
@@ -354,7 +502,11 @@ class JobCard(Document):
.format(bold(self.operation), work_order), OperationMismatchError)
def validate_sequence_id(self):
- if not (self.work_order and self.sequence_id): return
+ if self.is_corrective_job_card:
+ return
+
+ if not (self.work_order and self.sequence_id):
+ return
current_operation_qty = 0.0
data = self.get_current_operation_data()
@@ -376,6 +528,17 @@ class JobCard(Document):
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
+
+@frappe.whitelist()
+def make_time_log(args):
+ if isinstance(args, str):
+ args = json.loads(args)
+
+ args = frappe._dict(args)
+ doc = frappe.get_doc("Job Card", args.job_card_id)
+ doc.validate_sequence_id()
+ doc.add_time_log(args)
+
@frappe.whitelist()
def get_operation_details(work_order, operation):
if work_order and operation:
@@ -511,3 +674,28 @@ def get_job_details(start, end, filters=None):
events.append(job_card_data)
return events
+
+@frappe.whitelist()
+def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
+ def set_missing_values(source, target):
+ target.is_corrective_job_card = 1
+ target.operation = operation
+ target.for_operation = for_operation
+
+ target.set('time_logs', [])
+ target.set('employee', [])
+ target.set('items', [])
+ target.get_sub_operations()
+ target.get_required_items()
+ target.validate_time_logs()
+
+ doclist = get_mapped_doc("Job Card", source_name, {
+ "Job Card": {
+ "doctype": "Job Card",
+ "field_map": {
+ "name": "for_job_card",
+ },
+ }
+ }, target_doc, set_missing_values)
+
+ return doclist
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
index 100ef4ca3a3..d91530dd3b5 100644
--- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
+++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
@@ -25,8 +25,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
- "options": "Item",
- "read_only": 1
+ "options": "Item"
},
{
"fieldname": "source_warehouse",
@@ -67,8 +66,7 @@
"fieldname": "required_qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Required Qty",
- "read_only": 1
+ "label": "Required Qty"
},
{
"fieldname": "column_break_9",
@@ -107,7 +105,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-11 13:50:13.804108",
+ "modified": "2021-04-22 18:50:00.003444",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Item",
diff --git a/erpnext/manufacturing/doctype/job_card_operation/__init__.py b/erpnext/manufacturing/doctype/job_card_operation/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
new file mode 100644
index 00000000000..9a8692b84d9
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
@@ -0,0 +1,59 @@
+{
+ "actions": [],
+ "creation": "2020-12-07 16:58:38.449041",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "sub_operation",
+ "completed_time",
+ "status",
+ "completed_qty"
+ ],
+ "fields": [
+ {
+ "default": "Pending",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Complete\nPause\nPending\nWork In Progress",
+ "read_only": 1
+ },
+ {
+ "description": "In mins",
+ "fieldname": "completed_time",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Completed Time",
+ "read_only": 1
+ },
+ {
+ "fieldname": "sub_operation",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Operation",
+ "options": "Operation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "completed_qty",
+ "fieldtype": "Float",
+ "label": "Completed Qty",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-03-16 18:24:35.399593",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Job Card Operation",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py
new file mode 100644
index 00000000000..85d72982ed3
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class JobCardOperation(Document):
+ pass
diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
index 9dd54dd6182..a7102d7d237 100644
--- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
+++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
@@ -1,14 +1,17 @@
{
+ "actions": [],
"creation": "2019-03-08 23:56:43.187569",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "employee",
"from_time",
"to_time",
"column_break_2",
"time_in_mins",
- "completed_qty"
+ "completed_qty",
+ "operation"
],
"fields": [
{
@@ -41,10 +44,27 @@
"in_list_view": 1,
"label": "Completed Qty",
"reqd": 1
+ },
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Employee",
+ "options": "Employee"
+ },
+ {
+ "fieldname": "operation",
+ "fieldtype": "Link",
+ "label": "Operation",
+ "no_copy": 1,
+ "options": "Operation",
+ "read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-12-03 12:56:02.285448",
+ "links": [],
+ "modified": "2020-12-23 14:30:00.970916",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Time Log",
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index b7634da87c2..024f7847259 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -26,7 +26,10 @@
"column_break_16",
"overproduction_percentage_for_work_order",
"other_settings_section",
- "update_bom_costs_automatically"
+ "update_bom_costs_automatically",
+ "add_corrective_operation_cost_in_finished_good_valuation",
+ "column_break_23",
+ "make_serial_no_batch_from_work_order"
],
"fields": [
{
@@ -155,13 +158,30 @@
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order",
+ "fieldname": "make_serial_no_batch_from_work_order",
+ "fieldtype": "Check",
+ "label": "Make Serial No / Batch from Work Order"
+ },
+ {
+ "default": "0",
+ "fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
+ "fieldtype": "Check",
+ "label": "Add Corrective Operation Cost in Finished Good Valuation"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 10:55:43.996581",
+ "modified": "2021-03-16 15:54:38.967341",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
@@ -178,4 +198,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js
index 5c2aba6f095..102b6780e5f 100644
--- a/erpnext/manufacturing/doctype/operation/operation.js
+++ b/erpnext/manufacturing/doctype/operation/operation.js
@@ -2,7 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('Operation', {
- refresh: function(frm) {
-
+ setup: function(frm) {
+ frm.set_query('operation', 'sub_operations', function() {
+ return {
+ filters: {
+ 'name': ['not in', [frm.doc.name]]
+ }
+ };
+ });
}
-});
+});
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json
index c231fba2faa..10a97eda763 100644
--- a/erpnext/manufacturing/doctype/operation/operation.json
+++ b/erpnext/manufacturing/doctype/operation/operation.json
@@ -1,167 +1,132 @@
{
- "allow_copy": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "Prompt",
- "beta": 0,
- "creation": "2014-11-07 16:20:30.683186",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2014-11-07 16:20:30.683186",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "workstation",
+ "data_2",
+ "is_corrective_operation",
+ "job_card_section",
+ "create_job_card_based_on_batch_size",
+ "column_break_6",
+ "batch_size",
+ "sub_operations_section",
+ "sub_operations",
+ "total_operation_time",
+ "section_break_4",
+ "description"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "workstation",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Default Workstation",
- "length": 0,
- "no_copy": 0,
- "options": "Workstation",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "workstation",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Default Workstation",
+ "options": "Workstation"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 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,
- "unique": 0
- },
+ "collapsible": 1,
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "label": "Operation Description"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "sub_operations_section",
+ "fieldtype": "Section Break",
+ "label": "Sub Operations"
+ },
+ {
+ "fieldname": "sub_operations",
+ "fieldtype": "Table",
+ "options": "Sub Operation"
+ },
+ {
+ "description": "Time in mins.",
+ "fieldname": "total_operation_time",
+ "fieldtype": "Float",
+ "label": "Total Operation Time",
+ "read_only": 1
+ },
+ {
+ "fieldname": "data_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "1",
+ "depends_on": "create_job_card_based_on_batch_size",
+ "fieldname": "batch_size",
+ "fieldtype": "Int",
+ "label": "Batch Size",
+ "mandatory_depends_on": "create_job_card_based_on_batch_size"
+ },
+ {
+ "default": "0",
+ "fieldname": "create_job_card_based_on_batch_size",
+ "fieldtype": "Check",
+ "label": "Create Job Card based on Batch Size"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "job_card_section",
+ "fieldtype": "Section Break",
+ "label": "Job Card"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_corrective_operation",
+ "fieldtype": "Check",
+ "label": "Is Corrective Operation"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-wrench",
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2016-11-07 05:28:27.462413",
- "modified_by": "Administrator",
- "module": "Manufacturing",
- "name": "Operation",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-wrench",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-01-12 15:09:23.593338",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Operation",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 0,
- "export": 1,
- "if_owner": 0,
- "import": 1,
- "is_custom": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Manufacturing User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "export": 1,
+ "import": 1,
+ "read": 1,
+ "role": "Manufacturing User",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 0,
- "export": 1,
- "if_owner": 0,
- "import": 1,
- "is_custom": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 1,
- "role": "Manufacturing Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "export": 1,
+ "import": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py
index 69e83292ff8..374f32019bd 100644
--- a/erpnext/manufacturing/doctype/operation/operation.py
+++ b/erpnext/manufacturing/doctype/operation/operation.py
@@ -2,9 +2,34 @@
# For license information, please see license.txt
from __future__ import unicode_literals
+
+import frappe
+from frappe import _
from frappe.model.document import Document
class Operation(Document):
def validate(self):
if not self.description:
self.description = self.name
+
+ self.duplicate_sub_operation()
+ self.set_total_time()
+
+ def duplicate_sub_operation(self):
+ operation_list = []
+ for row in self.sub_operations:
+ if row.operation in operation_list:
+ frappe.throw(_("The operation {0} can not add multiple times")
+ .format(frappe.bold(row.operation)))
+
+ if self.name == row.operation:
+ frappe.throw(_("The operation {0} can not be the sub operation")
+ .format(frappe.bold(row.operation)))
+
+ operation_list.append(row.operation)
+
+ def set_total_time(self):
+ self.total_operation_time = 0.0
+
+ for row in self.sub_operations:
+ self.total_operation_time += row.time_in_mins
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 64d584118f2..d198a6962a5 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -4,7 +4,7 @@
frappe.ui.form.on('Production Plan', {
setup: function(frm) {
frm.custom_make_buttons = {
- 'Work Order': 'Work Order',
+ 'Work Order': 'Work Order / Subcontract PO',
'Material Request': 'Material Request',
};
@@ -68,17 +68,13 @@ frappe.ui.form.on('Production Plan', {
frm.trigger("show_progress");
if (frm.doc.status !== "Completed") {
- if (frm.doc.po_items && frm.doc.status !== "Closed") {
- frm.add_custom_button(__("Work Order"), ()=> {
- frm.trigger("make_work_order");
- }, __('Create'));
- }
+ frm.add_custom_button(__("Work Order Tree"), ()=> {
+ frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name});
+ }, __('View'));
- if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
- frm.add_custom_button(__("Material Request"), ()=> {
- frm.trigger("make_material_request");
- }, __('Create'));
- }
+ frm.add_custom_button(__("Production Plan Summary"), ()=> {
+ frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name});
+ }, __('View'));
if (frm.doc.status === "Closed") {
frm.add_custom_button(__("Re-open"), function() {
@@ -89,6 +85,18 @@ frappe.ui.form.on('Production Plan', {
frm.events.close_open_production_plan(frm, true);
}, __("Status"));
}
+
+ if (frm.doc.po_items && frm.doc.status !== "Closed") {
+ frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> {
+ frm.trigger("make_work_order");
+ }, __('Create'));
+ }
+
+ if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
+ frm.add_custom_button(__("Material Request"), ()=> {
+ frm.trigger("make_material_request");
+ }, __('Create'));
+ }
}
}
@@ -233,6 +241,17 @@ frappe.ui.form.on('Production Plan', {
});
},
+ get_sub_assembly_items: function(frm) {
+ frappe.call({
+ method: "get_sub_assembly_items",
+ freeze: true,
+ doc: frm.doc,
+ callback: function() {
+ refresh_field("sub_assembly_items");
+ }
+ });
+ },
+
get_items_for_mr: function(frm) {
if (!frm.doc.for_warehouse) {
frappe.throw(__("Select warehouse for material requests"));
@@ -306,8 +325,25 @@ frappe.ui.form.on('Production Plan', {
},
download_materials_required: function(frm) {
- let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
- open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc });
+ const fields = [{
+ fieldname: 'warehouses',
+ fieldtype: 'Table MultiSelect',
+ label: __('Warehouses'),
+ default: frm.doc.from_warehouse,
+ options: "Production Plan Material Request Warehouse",
+ get_query: function () {
+ return {
+ filters: {
+ company: frm.doc.company
+ }
+ };
+ },
+ }];
+
+ frappe.prompt(fields, (row) => {
+ let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
+ open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses: row.warehouses });
+ }, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock'));
},
show_progress: function(frm) {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 1c0dde227c5..84378956c61 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -32,6 +32,9 @@
"po_items",
"section_break_25",
"prod_plan_references",
+ "section_break_24",
+ "get_sub_assembly_items",
+ "sub_assembly_items",
"material_request_planning",
"include_non_stock_items",
"include_subcontracted_items",
@@ -187,7 +190,7 @@
"depends_on": "get_items_from",
"fieldname": "get_items",
"fieldtype": "Button",
- "label": "Get Items For Work Order"
+ "label": "Get Finished Goods for Manufacture"
},
{
"fieldname": "po_items",
@@ -199,7 +202,7 @@
{
"fieldname": "material_request_planning",
"fieldtype": "Section Break",
- "label": "Material Request Planning"
+ "label": "Material Requirement Planning"
},
{
"default": "1",
@@ -237,12 +240,13 @@
},
{
"fieldname": "section_break_27",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "hide_border": 1
},
{
"fieldname": "mr_items",
"fieldtype": "Table",
- "label": "Material Request Plan Item",
+ "label": "Raw Materials",
"no_copy": 1,
"options": "Material Request Plan Item"
},
@@ -337,13 +341,30 @@
"hidden": 1,
"label": "Production Plan Item Reference",
"options": "Production Plan Item Reference"
+ },
+ {
+ "fieldname": "section_break_24",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "fieldname": "sub_assembly_items",
+ "fieldtype": "Table",
+ "label": "Sub Assembly Items",
+ "no_copy": 1,
+ "options": "Production Plan Sub Assembly Item"
+ },
+ {
+ "fieldname": "get_sub_assembly_items",
+ "fieldtype": "Button",
+ "label": "Get Sub Assembly Items"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-05-24 16:59:03.643211",
+ "modified": "2021-06-28 20:00:33.905114",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 46e047654b5..38a0ee77ad7 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -5,10 +5,11 @@
from __future__ import unicode_literals
import frappe, json, copy
from frappe import msgprint, _
-from six import string_types, iteritems
+from six import iteritems
from frappe.model.document import Document
-from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil
+from frappe.utils import (flt, cint, nowdate, add_days, comma_and, now_datetime,
+ ceil, get_link_to_form, getdate)
from frappe.utils.csvutils import build_csv_response
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
@@ -98,7 +99,7 @@ class ProductionPlan(Document):
def get_items(self):
self.set('po_items', [])
if self.get_items_from == "Sales Order":
- self.get_so_items()
+ self.get_so_items()
elif self.get_items_from == "Material Request":
self.get_mr_items()
@@ -170,11 +171,11 @@ class ProductionPlan(Document):
refs = {}
for data in items:
item_details = get_item_details(data.item_code)
- if self.combine_items:
+ if self.combine_items:
if item_details.bom_no in refs:
refs[item_details.bom_no]['so_details'].append({
'sales_order': data.parent,
- 'sales_order_item': data.name,
+ 'sales_order_item': data.name,
'qty': data.pending_qty
})
refs[item_details.bom_no]['qty'] += data.pending_qty
@@ -188,10 +189,10 @@ class ProductionPlan(Document):
}
refs[item_details.bom_no]['so_details'].append({
'sales_order': data.parent,
- 'sales_order_item': data.name,
+ 'sales_order_item': data.name,
'qty': data.pending_qty
})
-
+
pi = self.append('po_items', {
'include_exploded_items': 1,
'warehouse': data.warehouse,
@@ -209,12 +210,12 @@ class ProductionPlan(Document):
pi.sales_order = data.parent
pi.sales_order_item = data.name
pi.description = data.description
-
+
elif self.get_items_from == "Material Request":
pi.material_request = data.parent
pi.material_request_item = data.name
pi.description = data.description
-
+
if refs:
for po_item in self.po_items:
po_item.planned_qty = refs[po_item.bom_no]['qty']
@@ -349,49 +350,88 @@ class ProductionPlan(Document):
@frappe.whitelist()
def make_work_order(self):
- wo_list = []
+ wo_list, po_list = [], []
+ subcontracted_po = {}
+
self.validate_data()
+ self.make_work_order_for_finished_goods(wo_list)
+ self.make_work_order_for_subassembly_items(wo_list, subcontracted_po)
+ self.make_subcontracted_purchase_order(subcontracted_po, po_list)
+ self.show_list_created_message('Work Order', wo_list)
+ self.show_list_created_message('Purchase Order', po_list)
+
+ def make_work_order_for_finished_goods(self, wo_list):
items_data = self.get_production_items()
for key, item in items_data.items():
+ if self.sub_assembly_items:
+ item['use_multi_level_bom'] = 0
+
work_order = self.create_work_order(item)
if work_order:
wo_list.append(work_order)
- if item.get("make_work_order_for_sub_assembly_items"):
- work_orders = self.make_work_order_for_sub_assembly_items(item)
- wo_list.extend(work_orders)
+ def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po):
+ for row in self.sub_assembly_items:
+ if row.type_of_manufacturing == 'Subcontract':
+ subcontracted_po.setdefault(row.supplier, []).append(row)
+ continue
+
+ args = {}
+ self.prepare_args_for_sub_assembly_items(row, args)
+ work_order = self.create_work_order(args)
+ if work_order:
+ wo_list.append(work_order)
+
+ def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
+ if not subcontracted_po:
+ return
+
+ for supplier, po_list in subcontracted_po.items():
+ po = frappe.new_doc('Purchase Order')
+ po.supplier = supplier
+ po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
+ po.is_subcontracted_item = 'Yes'
+ for row in po_list:
+ args = {
+ 'item_code': row.production_item,
+ 'warehouse': row.fg_warehouse,
+ 'production_plan_sub_assembly_item': row.name,
+ 'bom': row.bom_no,
+ 'production_plan': self.name
+ }
+
+ for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name',
+ 'description', 'production_plan_item']:
+ args[field] = row.get(field)
+
+ po.append('items', args)
+
+ po.set_missing_values()
+ po.flags.ignore_mandatory = True
+ po.flags.ignore_validate = True
+ po.insert()
+ purchase_orders.append(po.name)
+
+ def show_list_created_message(self, doctype, doc_list=None):
+ if not doc_list:
+ return
frappe.flags.mute_messages = False
+ if doc_list:
+ doc_list = [get_link_to_form(doctype, p) for p in doc_list]
+ msgprint(_("{0} created").format(comma_and(doc_list)))
- 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 prepare_args_for_sub_assembly_items(self, row, args):
+ for field in ["production_item", "item_name", "qty", "fg_warehouse",
+ "description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
+ args[field] = row.get(field)
- def make_work_order_for_sub_assembly_items(self, item):
- work_orders = []
- bom_data = {}
-
- get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty"))
-
- for key, data in bom_data.items():
- data.update({
- 'qty': data.get("stock_qty"),
- 'production_plan': self.name,
- 'use_multi_level_bom': item.get("use_multi_level_bom"),
- 'company': self.company,
- 'fg_warehouse': item.get("fg_warehouse"),
- 'update_consumed_material_cost_in_project': 0
- })
-
- work_order = self.create_work_order(data)
- if work_order:
- work_orders.append(work_order)
-
- return work_orders
+ args.update({
+ "use_multi_level_bom": 0,
+ "production_plan": self.name,
+ "production_plan_sub_assembly_item": row.name
+ })
def create_work_order(self, item):
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse
@@ -476,19 +516,43 @@ class ProductionPlan(Document):
else :
msgprint(_("No material request created"))
+ @frappe.whitelist()
+ def get_sub_assembly_items(self, manufacturing_type=None):
+ self.sub_assembly_items = []
+ for row in self.po_items:
+ bom_data = []
+ get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
+ self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
+
+ self.save()
+
+ def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
+ bom_data = sorted(bom_data, key = lambda i: i.bom_level)
+
+ for data in bom_data:
+ data.qty = data.stock_qty
+ data.production_plan_item = row.name
+ data.fg_warehouse = row.warehouse
+ data.schedule_date = row.planned_start_date
+ data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
+ else "In House")
+
+ self.append("sub_assembly_items", data)
+
@frappe.whitelist()
-def download_raw_materials(doc):
- if isinstance(doc, string_types):
+def download_raw_materials(doc, warehouses=None):
+ if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
- 'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production',
- 'Safety Stock', 'Required Qty']]
+ 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty',
+ 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']]
- for d in get_items_for_material_requests(doc):
+ doc.warehouse = None
+ for d in get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True):
item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'),
d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
- d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
+ d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
if not doc.get('for_warehouse'):
row = {'item_code': d.get('item_code')}
@@ -507,7 +571,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name,
bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
item.default_material_request_type, item.min_order_qty, item_default.default_warehouse,
- item.purchase_uom, item_uom.conversion_factor
+ item.purchase_uom, item_uom.conversion_factor, item.safety_stock
from
`tabBOM Explosion Item` bei
JOIN `tabBOM` bom ON bom.name = bei.parent
@@ -659,7 +723,7 @@ def get_sales_orders(self):
@frappe.whitelist()
def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
- if isinstance(row, string_types):
+ if isinstance(row, str):
row = frappe._dict(json.loads(row))
company = frappe.db.escape(company)
@@ -677,32 +741,39 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
- ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin`
- where item_code = %(item_code)s {conditions}
+ ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse,
+ ifnull(sum(planned_qty),0) as planned_qty
+ from `tabBin` where item_code = %(item_code)s {conditions}
group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
+def get_warehouse_list(warehouses, warehouse_list=None):
+ if not warehouse_list:
+ warehouse_list = []
+
+ if isinstance(warehouses, str):
+ warehouses = json.loads(warehouses)
+
+ for row in warehouses:
+ child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
+ if child_warehouses:
+ warehouse_list.extend(child_warehouses)
+ else:
+ warehouse_list.append(row.get("warehouse"))
+
@frappe.whitelist()
-def get_items_for_material_requests(doc, warehouses=None):
- if isinstance(doc, string_types):
+def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
+ if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
warehouse_list = []
if warehouses:
- if isinstance(warehouses, string_types):
- warehouses = json.loads(warehouses)
-
- for row in warehouses:
- child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
- if child_warehouses:
- warehouse_list.extend(child_warehouses)
- else:
- warehouse_list.append(row.get("warehouse"))
+ get_warehouse_list(warehouses, warehouse_list)
if warehouse_list:
warehouses = list(set(warehouse_list))
- if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses:
+ if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses:
warehouses.remove(doc.get("for_warehouse"))
warehouse_list = None
@@ -721,6 +792,9 @@ def get_items_for_material_requests(doc, warehouses=None):
so_item_details = frappe._dict()
for data in po_items:
+ if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
+ data["include_exploded_items"] = 1
+
planned_qty = data.get('required_qty') or data.get('planned_qty')
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty
warehouse = doc.get('for_warehouse')
@@ -795,7 +869,7 @@ def get_items_for_material_requests(doc, warehouses=None):
if items:
mr_items.append(items)
- if not ignore_existing_ordered_qty and warehouses:
+ if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses:
new_mr_items = []
for item in mr_items:
get_materials_from_other_locations(item, warehouses, new_mr_items, company)
@@ -852,23 +926,28 @@ def get_item_data(item_code):
# "description": item_details.get("description")
}
-def get_sub_assembly_items(bom_no, bom_data, to_produce_qty):
+def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
data = get_children('BOM', parent = bom_no)
for d in data:
if d.expandable:
- key = (d.name, d.value)
- if key not in bom_data:
- bom_data.setdefault(key, {
- 'stock_qty': 0,
- 'description': d.description,
- 'production_item': d.item_code,
- 'item_name': d.item_name,
- 'stock_uom': d.stock_uom,
- 'uom': d.stock_uom,
- 'bom_no': d.value
- })
+ parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
+ bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
+ if d.value else 0)
- bom_item = bom_data.get(key)
- bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
+ stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
+ bom_data.append(frappe._dict({
+ 'parent_item_code': parent_item_code,
+ 'description': d.description,
+ 'production_item': d.item_code,
+ 'item_name': d.item_name,
+ 'stock_uom': d.stock_uom,
+ 'uom': d.stock_uom,
+ 'bom_no': d.value,
+ 'is_sub_contracted_item': d.is_sub_contracted_item,
+ 'bom_level': bom_level,
+ 'indent': indent,
+ 'stock_qty': stock_qty
+ }))
- get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"])
+ if d.value:
+ get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py
index 09ec24a67a2..ca597f63278 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py
@@ -9,5 +9,9 @@ def get_data():
'label': _('Transactions'),
'items': ['Work Order', 'Material Request']
},
+ {
+ 'label': _('Subcontract'),
+ 'items': ['Purchase Order']
+ },
]
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 768f99eb431..cce1bb61b6b 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -169,7 +169,7 @@ class TestProductionPlan(unittest.TestCase):
pln.get_items()
pln.submit()
- self.assertTrue(pln.po_items[0].planned_qty, 3)
+ self.assertTrue(pln.po_items[0].planned_qty, 3)
pln.make_work_order()
work_order = frappe.db.get_value('Work Order', {
@@ -193,10 +193,10 @@ class TestProductionPlan(unittest.TestCase):
for so_item in so_items:
so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
self.assertEqual(so_wo_qty, 0.0)
-
+
latest_plan = frappe.get_doc('Production Plan', pln.name)
latest_plan.cancel()
-
+
def test_pp_to_mr_customer_provided(self):
#Material Request from Production Plan for Customer Provided
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
@@ -236,10 +236,10 @@ class TestProductionPlan(unittest.TestCase):
pln.append("po_items", {
"item_code": item_code,
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
- "planned_qty": 3,
- "make_work_order_for_sub_assembly_items": 1
+ "planned_qty": 3
})
+ pln.get_sub_assembly_items('In House')
pln.submit()
pln.make_work_order()
diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
index 89ab7aa0a06..f829d57475a 100644
--- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
@@ -9,18 +9,17 @@
"include_exploded_items",
"item_code",
"bom_no",
- "planned_qty",
"column_break_6",
- "make_work_order_for_sub_assembly_items",
+ "planned_qty",
"warehouse",
"planned_start_date",
"section_break_9",
"pending_qty",
"ordered_qty",
- "produced_qty",
"column_break_17",
"description",
"stock_uom",
+ "produced_qty",
"reference_section",
"sales_order",
"sales_order_item",
@@ -32,11 +31,10 @@
],
"fields": [
{
- "columns": 2,
- "default": "0",
+ "columns": 1,
+ "default": "1",
"fieldname": "include_exploded_items",
"fieldtype": "Check",
- "in_list_view": 1,
"label": "Include Exploded Items"
},
{
@@ -80,13 +78,6 @@
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
- {
- "default": "0",
- "description": "If enabled, system will create the work order for the exploded items against which BOM is available.",
- "fieldname": "make_work_order_for_sub_assembly_items",
- "fieldtype": "Check",
- "label": "Make Work Order for Sub Assembly Items"
- },
{
"fieldname": "warehouse",
"fieldtype": "Link",
@@ -218,7 +209,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-28 19:14:57.772123",
+ "modified": "2021-06-28 18:31:06.822168",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
new file mode 100644
index 00000000000..657ee35a852
--- /dev/null
+++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
@@ -0,0 +1,202 @@
+{
+ "actions": [],
+ "creation": "2020-12-27 16:08:36.127199",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "production_item",
+ "item_name",
+ "fg_warehouse",
+ "parent_item_code",
+ "schedule_date",
+ "column_break_3",
+ "qty",
+ "bom_no",
+ "bom_level",
+ "type_of_manufacturing",
+ "supplier",
+ "work_order_details_section",
+ "work_order",
+ "purchase_order",
+ "production_plan_item",
+ "column_break_7",
+ "produced_qty",
+ "received_qty",
+ "indent",
+ "section_break_19",
+ "uom",
+ "stock_uom",
+ "column_break_22",
+ "description"
+ ],
+ "fields": [
+ {
+ "fetch_from": "sub_assembly_item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.type_of_manufacturing == \"In House\"",
+ "fieldname": "work_order_details_section",
+ "fieldtype": "Section Break",
+ "label": "Reference"
+ },
+ {
+ "fieldname": "work_order",
+ "fieldtype": "Link",
+ "label": "Work Order",
+ "options": "Work Order",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 1,
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Required Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "purchase_order",
+ "fieldtype": "Link",
+ "label": "Purchase Order",
+ "options": "Purchase Order",
+ "read_only": 1
+ },
+ {
+ "fieldname": "received_qty",
+ "fieldtype": "Float",
+ "label": "Received Qty"
+ },
+ {
+ "fieldname": "bom_no",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Bom No",
+ "options": "BOM"
+ },
+ {
+ "fieldname": "production_plan_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Production Plan Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "parent_item_code",
+ "fieldtype": "Link",
+ "label": "Finished Good",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "columns": 1,
+ "fetch_from": "bom_no.bom_level",
+ "fieldname": "bom_level",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Level (BOM)",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_19",
+ "fieldtype": "Section Break",
+ "label": "Item Details"
+ },
+ {
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_22",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "description",
+ "read_only": 1
+ },
+ {
+ "fieldname": "production_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Sub Assembly Item Code",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "indent",
+ "fieldtype": "Int",
+ "label": "Indent"
+ },
+ {
+ "fieldname": "fg_warehouse",
+ "fieldtype": "Link",
+ "label": "Target Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "produced_qty",
+ "fieldtype": "Data",
+ "label": "Produced Quantity",
+ "read_only": 1
+ },
+ {
+ "default": "In House",
+ "fieldname": "type_of_manufacturing",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Manufacturing Type",
+ "options": "In House\nSubcontract"
+ },
+ {
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "label": "Supplier",
+ "mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'",
+ "options": "Supplier"
+ },
+ {
+ "fieldname": "schedule_date",
+ "fieldtype": "Datetime",
+ "in_list_view": 1,
+ "label": "Schedule Date"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-06-28 20:10:56.296410",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Production Plan Sub Assembly Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py
new file mode 100644
index 00000000000..6850a2eb4ed
--- /dev/null
+++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ProductionPlanSubAssemblyItem(Document):
+ pass
diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py
index 8312d7436c2..ece0db717a2 100644
--- a/erpnext/manufacturing/doctype/routing/routing.py
+++ b/erpnext/manufacturing/doctype/routing/routing.py
@@ -4,14 +4,24 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import cint
+from frappe.utils import cint, flt
from frappe import _
from frappe.model.document import Document
class Routing(Document):
def validate(self):
+ self.calculate_operating_cost()
self.set_routing_id()
+ def on_update(self):
+ self.calculate_operating_cost()
+
+ def calculate_operating_cost(self):
+ for operation in self.operations:
+ if not operation.hour_rate:
+ operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
+ operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
+
def set_routing_id(self):
sequence_id = 0
for row in self.operations:
@@ -21,4 +31,4 @@ class Routing(Document):
frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}")
.format(row.idx, row.sequence_id, sequence_id))
- sequence_id = row.sequence_id
\ No newline at end of file
+ sequence_id = row.sequence_id
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index 6a38dcfa030..92f26946ab7 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -7,9 +7,7 @@ import unittest
import frappe
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.item.test_item import make_item
-from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
-from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
class TestRouting(unittest.TestCase):
@@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase):
wo_doc.cancel()
wo_doc.delete()
+ def test_update_bom_operation_time(self):
+ operations = [
+ {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "hour_rate_rent": 300,
+ "hour_rate_labour": 750 ,
+ "time_in_mins": 30
+ },
+ {
+ "operation": "Test Operation B",
+ "workstation": "_Test Workstation B",
+ "hour_rate_labour": 200,
+ "hour_rate_rent": 1000,
+ "time_in_mins": 20
+ }
+ ]
+
+ test_routing_operations = [
+ {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "time_in_mins": 30
+ },
+ {
+ "operation": "Test Operation B",
+ "workstation": "_Test Workstation A",
+ "time_in_mins": 20
+ }
+ ]
+ setup_operations(operations)
+ routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations)
+ bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR')
+ self.assertEqual(routing_doc.operations[0].time_in_mins, 30)
+ self.assertEqual(routing_doc.operations[1].time_in_mins, 20)
+ routing_doc.operations[0].time_in_mins = 90
+ routing_doc.operations[1].time_in_mins = 42.2
+ routing_doc.save()
+ bom_doc.update_cost()
+ bom_doc.reload()
+ self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
+ self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
+
+
def setup_operations(rows):
+ from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
+ from erpnext.manufacturing.doctype.operation.test_operation import make_operation
for row in rows:
make_workstation(row)
make_operation(row)
@@ -61,12 +105,14 @@ def create_routing(**args):
if not args.do_not_save:
try:
- for operation in args.operations:
- doc.append("operations", operation)
-
doc.insert()
except frappe.DuplicateEntryError:
doc = frappe.get_doc("Routing", args.routing_name)
+ doc.delete_key('operations')
+ for operation in args.operations:
+ doc.append("operations", operation)
+
+ doc.save()
return doc
@@ -91,7 +137,7 @@ def setup_bom(**args):
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
if not name:
bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"),
- routing = args.routing, with_operations=1)
+ routing = args.routing, with_operations=1, currency = args.currency)
else:
bom_doc = frappe.get_doc("BOM", name)
diff --git a/erpnext/manufacturing/doctype/sub_operation/__init__.py b/erpnext/manufacturing/doctype/sub_operation/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.js b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js
new file mode 100644
index 00000000000..be9db6a4089
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Sub Operation', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
new file mode 100644
index 00000000000..f63d2b98641
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "creation": "2020-12-07 15:39:47.488519",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "operation",
+ "time_in_mins",
+ "column_break_5",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "operation",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Operation",
+ "options": "Operation"
+ },
+ {
+ "description": "Time in mins",
+ "fieldname": "time_in_mins",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Operation Time"
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-12-07 18:09:18.005578",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Sub Operation",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py
new file mode 100644
index 00000000000..f4b27758e9d
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class SubOperation(Document):
+ pass
diff --git a/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py
new file mode 100644
index 00000000000..d3410ca3120
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestSubOperation(unittest.TestCase):
+ pass
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index cb1ee92196f..68de0b29d3e 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase):
ste.submit()
stock_entries.append(ste)
- job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
+ job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc')
self.assertEqual(len(job_cards), len(bom.operations))
for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card)
- doc.append("time_logs", {
- "from_time": add_to_date(None, i),
- "hours": 1,
- "to_time": add_to_date(None, i + 1),
- "completed_qty": doc.for_quantity
- })
+ doc.time_logs[0].completed_qty = 1
doc.submit()
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 3e5a72db9a7..512048512ed 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", {
}
if (frm.doc.docstatus === 1
- && frm.doc.operations && frm.doc.operations.length
- && frm.doc.qty != frm.doc.material_transferred_for_manufacturing) {
+ && frm.doc.operations && frm.doc.operations.length) {
const not_completed = frm.doc.operations.filter(d => {
if(d.status != 'Completed') {
@@ -190,35 +189,41 @@ frappe.ui.form.on("Work Order", {
const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
fields: [
{
- fieldtype:'Link',
- fieldname:'operation',
+ fieldtype: 'Link',
+ fieldname: 'operation',
label: __('Operation'),
- read_only:1,
- in_list_view:1
+ read_only: 1,
+ in_list_view: 1
},
{
- fieldtype:'Link',
- fieldname:'workstation',
+ fieldtype: 'Link',
+ fieldname: 'workstation',
label: __('Workstation'),
- read_only:1,
- in_list_view:1
+ read_only: 1,
+ in_list_view: 1
},
{
- fieldtype:'Data',
- fieldname:'name',
+ fieldtype: 'Data',
+ fieldname: 'name',
label: __('Operation Id')
},
{
- fieldtype:'Float',
- fieldname:'pending_qty',
+ fieldtype: 'Float',
+ fieldname: 'pending_qty',
label: __('Pending Qty'),
},
{
- fieldtype:'Float',
- fieldname:'qty',
+ fieldtype: 'Float',
+ fieldname: 'qty',
label: __('Quantity to Manufacture'),
- read_only:0,
- in_list_view:1,
+ read_only: 0,
+ in_list_view: 1,
+ },
+ {
+ fieldtype: 'Float',
+ fieldname: 'batch_size',
+ label: __('Batch Size'),
+ read_only: 1
},
],
data: operations_data,
@@ -229,9 +234,13 @@ frappe.ui.form.on("Work Order", {
}, function(data) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
+ freeze: true,
args: {
work_order: frm.doc.name,
operations: data.operations,
+ },
+ callback: function() {
+ frm.reload_doc();
}
});
}, __("Job Card"), __("Create"));
@@ -243,13 +252,16 @@ frappe.ui.form.on("Work Order", {
if(data.completed_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty);
- dialog.fields_dict.operations.df.data.push({
- 'name': data.name,
- 'operation': data.operation,
- 'workstation': data.workstation,
- 'qty': pending_qty,
- 'pending_qty': pending_qty,
- });
+ if (pending_qty) {
+ dialog.fields_dict.operations.df.data.push({
+ 'name': data.name,
+ 'operation': data.operation,
+ 'workstation': data.workstation,
+ 'batch_size': data.batch_size,
+ 'qty': pending_qty,
+ 'pending_qty': pending_qty
+ });
+ }
}
});
dialog.fields_dict.operations.grid.refresh();
@@ -704,6 +716,8 @@ erpnext.work_order = {
stop_work_order: function(frm, status) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",
+ freeze: true,
+ freeze_message: __("Updating Work Order status"),
args: {
work_order: frm.doc.name,
status: status
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index cd9edeeea83..3b56854aaf3 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -21,6 +21,12 @@
"produced_qty",
"sales_order",
"project",
+ "serial_no_and_batch_for_finished_good_section",
+ "has_serial_no",
+ "has_batch_no",
+ "column_break_17",
+ "serial_no",
+ "batch_size",
"settings_section",
"allow_alternative_item",
"use_multi_level_bom",
@@ -52,16 +58,22 @@
"actual_operating_cost",
"additional_operating_cost",
"column_break_24",
+ "corrective_operation_cost",
"total_operating_cost",
"more_info",
"description",
"stock_uom",
"column_break2",
+ "references_section",
"material_request",
"material_request_item",
"sales_order_item",
+ "column_break_61",
"production_plan",
"production_plan_item",
+ "production_plan_sub_assembly_item",
+ "parent_work_order",
+ "bom_level",
"product_bundle_item",
"amended_from"
],
@@ -488,17 +500,77 @@
"fieldtype": "Float",
"label": "Lead Time",
"read_only": 1
- }
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "serial_no_and_batch_for_finished_good_section",
+ "fieldtype": "Section Break",
+ "label": "Serial No and Batch for Finished Good"
+ },
+ {
+ "fieldname": "column_break_17",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fetch_from": "production_item.has_serial_no",
+ "fieldname": "has_serial_no",
+ "fieldtype": "Check",
+ "label": "Has Serial No",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fetch_from": "production_item.has_batch_no",
+ "fieldname": "has_batch_no",
+ "fieldtype": "Check",
+ "label": "Has Batch No",
+ "read_only": 1
+ },
+ {
+ "depends_on": "has_serial_no",
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "label": "Serial Nos",
+ "no_copy": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "has_batch_no",
+ "fieldname": "batch_size",
+ "fieldtype": "Float",
+ "label": "Batch Size"
+ },
+ {
+ "allow_on_submit": 1,
+ "description": "From Corrective Job Card",
+ "fieldname": "corrective_operation_cost",
+ "fieldtype": "Currency",
+ "label": "Corrective Operation Cost",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "production_plan_sub_assembly_item",
+ "fieldtype": "Data",
+ "label": "Production Plan Sub-assembly Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ }
],
"icon": "fa fa-cogs",
"idx": 1,
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-03-16 13:27:51.116484",
+ "modified": "2021-06-28 16:19:14.902699",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
+ "nsm_parent_field": "parent_work_order",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 2600790a59a..779ae42d653 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1,7 +1,6 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
import frappe
import json
import math
@@ -19,18 +18,17 @@ from frappe.utils.csvutils import getlink
from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty
from erpnext.utilities.transaction_base import validate_uom_is_integer
from frappe.model.mapper import get_mapped_doc
+from erpnext.stock.doctype.batch.batch import make_batch
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos
class OverProductionError(frappe.ValidationError): pass
class CapacityError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass
class OperationTooLongError(frappe.ValidationError): pass
class ItemHasVariantError(frappe.ValidationError): pass
+class SerialNoQtyError(frappe.ValidationError):
+ pass
-from six import string_types
-
-form_grid_templates = {
- "operations": "templates/form_grid/work_order_grid.html"
-}
class WorkOrder(Document):
def onload(self):
@@ -127,7 +125,9 @@ class WorkOrder(Document):
variable_cost = self.actual_operating_cost if self.actual_operating_cost \
else self.planned_operating_cost
- self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost)
+
+ self.total_operating_cost = (flt(self.additional_operating_cost)
+ + flt(variable_cost) + flt(self.corrective_operation_cost))
def validate_work_order_against_so(self):
# already ordered qty
@@ -235,12 +235,15 @@ class WorkOrder(Document):
production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item)
+ def before_submit(self):
+ self.create_serial_no_batch_no()
+
def on_submit(self):
if not self.wip_warehouse:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse:
frappe.throw(_("For Warehouse is required before Submit"))
-
+
if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}):
self.update_work_order_qty_in_combined_so()
else:
@@ -260,12 +263,76 @@ class WorkOrder(Document):
self.update_work_order_qty_in_combined_so()
else:
self.update_work_order_qty_in_so()
-
+
self.delete_job_card()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
self.update_ordered_qty()
self.update_reserved_qty_for_production()
+ self.delete_auto_created_batch_and_serial_no()
+
+ def create_serial_no_batch_no(self):
+ if not (self.has_serial_no or self.has_batch_no):
+ return
+
+ if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
+ return
+
+ if self.has_batch_no:
+ self.create_batch_for_finished_good()
+
+ args = {
+ "item_code": self.production_item,
+ "work_order": self.name
+ }
+
+ if self.has_serial_no:
+ self.make_serial_nos(args)
+
+ def create_batch_for_finished_good(self):
+ total_qty = self.qty
+ if not self.batch_size:
+ self.batch_size = total_qty
+
+ while total_qty > 0:
+ qty = self.batch_size
+ if self.batch_size >= total_qty:
+ qty = total_qty
+
+ if total_qty > self.batch_size:
+ total_qty -= self.batch_size
+ else:
+ qty = total_qty
+ total_qty = 0
+
+ make_batch(frappe._dict({
+ "item": self.production_item,
+ "qty_to_produce": qty,
+ "reference_doctype": self.doctype,
+ "reference_name": self.name
+ }))
+
+ def delete_auto_created_batch_and_serial_no(self):
+ for row in frappe.get_all("Serial No", filters = {"work_order": self.name}):
+ frappe.delete_doc("Serial No", row.name)
+ self.db_set("serial_no", "")
+
+ for row in frappe.get_all("Batch", filters = {"reference_name": self.name}):
+ frappe.delete_doc("Batch", row.name)
+
+ def make_serial_nos(self, args):
+ serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
+ if serial_no_series:
+ self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
+
+ if self.serial_no:
+ args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
+ auto_make_serial_nos(args)
+
+ serial_nos_length = len(get_serial_nos(self.serial_no))
+ if serial_nos_length != self.qty:
+ frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.")
+ .format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError)
def create_job_card(self):
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
@@ -273,32 +340,40 @@ class WorkOrder(Document):
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
- for i, row in enumerate(self.operations):
- self.set_operation_start_end_time(i, row)
-
- if not row.workstation:
- frappe.throw(_("Row {0}: select the workstation against the operation {1}")
- .format(row.idx, row.operation))
-
- original_start_time = row.planned_start_time
- job_card_doc = create_job_card(self, row,
- enable_capacity_planning=enable_capacity_planning, auto_create=True)
-
- if enable_capacity_planning and job_card_doc:
- row.planned_start_time = job_card_doc.time_logs[-1].from_time
- row.planned_end_time = job_card_doc.time_logs[-1].to_time
-
- if date_diff(row.planned_start_time, original_start_time) > plan_days:
- frappe.message_log.pop()
- frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
- .format(plan_days, row.operation), CapacityError)
-
- row.db_update()
+ for index, row in enumerate(self.operations):
+ qty = self.qty
+ while qty > 0:
+ qty = split_qty_based_on_batch_size(self, row, qty)
+ if row.job_card_qty > 0:
+ self.prepare_data_for_job_card(row, index,
+ plan_days, enable_capacity_planning)
planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date:
self.db_set("planned_end_date", planned_end_date)
+ def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
+ self.set_operation_start_end_time(index, row)
+
+ if not row.workstation:
+ frappe.throw(_("Row {0}: select the workstation against the operation {1}")
+ .format(row.idx, row.operation))
+
+ original_start_time = row.planned_start_time
+ job_card_doc = create_job_card(self, row, auto_create=True,
+ enable_capacity_planning=enable_capacity_planning)
+
+ if enable_capacity_planning and job_card_doc:
+ row.planned_start_time = job_card_doc.time_logs[-1].from_time
+ row.planned_end_time = job_card_doc.time_logs[-1].to_time
+
+ if date_diff(row.planned_start_time, original_start_time) > plan_days:
+ frappe.message_log.pop()
+ frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
+ .format(plan_days, row.operation), CapacityError)
+
+ row.db_update()
+
def set_operation_start_end_time(self, idx, row):
"""Set start and end time for given operation. If first operation, set start as
`planned_start_date`, else add time diff to end time of earlier operation."""
@@ -365,7 +440,7 @@ class WorkOrder(Document):
work_order_qty = qty[0][0] if qty and qty[0][0] else 0
frappe.db.set_value('Sales Order Item',
self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2))
-
+
def update_work_order_qty_in_combined_so(self):
total_bundle_qty = 1
if self.product_bundle_item:
@@ -378,7 +453,7 @@ class WorkOrder(Document):
prod_plan = frappe.get_doc('Production Plan', self.production_plan)
item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item')
-
+
for plan_reference in prod_plan.prod_plan_references:
work_order_qty = 0.0
if plan_reference.item_reference == item_reference:
@@ -386,53 +461,54 @@ class WorkOrder(Document):
work_order_qty = flt(plan_reference.qty) / total_bundle_qty
frappe.db.set_value('Sales Order Item',
plan_reference.sales_order_item, 'work_order_qty', work_order_qty)
-
+
def update_completed_qty_in_material_request(self):
if self.material_request:
frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item])
def set_work_order_operations(self):
"""Fetch operations from BOM and set in 'Work Order'"""
- self.set('operations', [])
- if not self.bom_no:
+ def _get_operations(bom_no, qty=1):
+ return frappe.db.sql(
+ f"""select
+ operation, description, workstation, idx,
+ base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
+ "Pending" as status, parent as bom, batch_size, sequence_id
+ from
+ `tabBOM Operation`
+ where
+ parent = %s order by idx
+ """, bom_no, as_dict=1)
+
+
+ self.set('operations', [])
+ if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'):
return
- if self.use_multi_level_bom:
- bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree()
+ operations = []
+ if not self.use_multi_level_bom:
+ bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
+ operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
else:
- bom_list = [self.bom_no]
+ bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
+ bom_traversal = list(reversed(bom_tree.level_order_traversal()))
+ bom_traversal.append(bom_tree) # add operation on top level item last
+
+ for d in bom_traversal:
+ if d.is_bom:
+ operations.extend(_get_operations(d.name, qty=d.exploded_qty))
+
+ for correct_index, operation in enumerate(operations, start=1):
+ operation.idx = correct_index
- operations = frappe.db.sql("""
- select
- operation, description, workstation, idx,
- base_hour_rate as hour_rate, time_in_mins,
- "Pending" as status, parent as bom, batch_size, sequence_id
- from
- `tabBOM Operation`
- where
- parent in (%s) order by idx
- """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1)
self.set('operations', operations)
-
- if self.use_multi_level_bom and self.get('operations') and self.get('items'):
- raw_material_operations = [d.operation for d in self.get('items')]
- operations = [d.operation for d in self.get('operations')]
-
- for operation in raw_material_operations:
- if operation not in operations:
- self.append('operations', {
- 'operation': operation
- })
-
self.calculate_time()
def calculate_time(self):
- bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
-
for d in self.get("operations"):
- d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size))
+ d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
self.calculate_operating_cost()
@@ -514,6 +590,7 @@ class WorkOrder(Document):
def validate_operation_time(self):
for d in self.operations:
if not d.time_in_mins > 0:
+ print(self.bom_no, self.production_item)
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
def update_required_items(self):
@@ -669,6 +746,17 @@ class WorkOrder(Document):
bom.set_bom_material_details()
return bom
+ def update_batch_produced_qty(self, stock_entry_doc):
+ if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
+ return
+
+ for row in stock_entry_doc.items:
+ if row.batch_no and (row.is_finished_item or row.is_scrap_item):
+ qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1},
+ or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0]
+
+ frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty))
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
@@ -746,7 +834,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None):
return wo_doc
def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
- if isinstance(variant_items, string_types):
+ if isinstance(variant_items, str):
variant_items = json.loads(variant_items)
for item in variant_items:
@@ -826,6 +914,7 @@ def make_stock_entry(work_order_id, purpose, qty=None):
stock_entry.set_stock_entry_type()
stock_entry.get_items()
+ stock_entry.set_serial_no_batch_for_finished_good()
return stock_entry.as_dict()
@frappe.whitelist()
@@ -867,13 +956,47 @@ def query_sales_order(production_item):
@frappe.whitelist()
def make_job_card(work_order, operations):
- if isinstance(operations, string_types):
+ if isinstance(operations, str):
operations = json.loads(operations)
work_order = frappe.get_doc('Work Order', work_order)
for row in operations:
+ row = frappe._dict(row)
validate_operation_data(row)
- create_job_card(work_order, row, row.get("qty"), auto_create=True)
+ qty = row.get("qty")
+ while qty > 0:
+ qty = split_qty_based_on_batch_size(work_order, row, qty)
+ if row.job_card_qty > 0:
+ create_job_card(work_order, row, auto_create=True)
+
+def split_qty_based_on_batch_size(wo_doc, row, qty):
+ if not cint(frappe.db.get_value("Operation",
+ row.operation, "create_job_card_based_on_batch_size")):
+ row.batch_size = row.get("qty") or wo_doc.qty
+
+ row.job_card_qty = row.batch_size
+ if row.batch_size and qty >= row.batch_size:
+ qty -= row.batch_size
+ elif qty > 0:
+ row.job_card_qty = qty
+ qty = 0
+
+ get_serial_nos_for_job_card(row, wo_doc)
+
+ return qty
+
+def get_serial_nos_for_job_card(row, wo_doc):
+ if not wo_doc.serial_no:
+ return
+
+ serial_nos = get_serial_nos(wo_doc.serial_no)
+ used_serial_nos = []
+ for d in frappe.get_all('Job Card', fields=['serial_no'],
+ filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}):
+ used_serial_nos.extend(get_serial_nos(d.serial_no))
+
+ serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos)))
+ row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty])
def validate_operation_data(row):
if row.get("qty") <= 0:
@@ -892,20 +1015,22 @@ def validate_operation_data(row):
)
)
-def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False):
+def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False):
doc = frappe.new_doc("Job Card")
doc.update({
'work_order': work_order.name,
'operation': row.get("operation"),
'workstation': row.get("workstation"),
'posting_date': nowdate(),
- 'for_quantity': qty or work_order.get('qty', 0),
+ 'for_quantity': row.job_card_qty or work_order.get('qty', 0),
'operation_id': row.get("name"),
'bom_no': work_order.bom_no,
'project': work_order.project,
'company': work_order.company,
'sequence_id': row.get("sequence_id"),
- 'wip_warehouse': work_order.wip_warehouse
+ 'wip_warehouse': work_order.wip_warehouse,
+ 'hour_rate': row.get("hour_rate"),
+ 'serial_no': row.get("serial_no")
})
if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer:
diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
index 87c090f99c3..9aa0715e7ff 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
@@ -4,10 +4,17 @@ from frappe import _
def get_data():
return {
'fieldname': 'work_order',
+ 'non_standard_fieldnames': {
+ 'Batch': 'reference_name'
+ },
'transactions': [
{
'label': _('Transactions'),
'items': ['Stock Entry', 'Job Card', 'Pick List']
+ },
+ {
+ 'label': _('Reference'),
+ 'items': ['Serial No', 'Batch']
}
]
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index 8c5cde9a13c..f7b8787a0b3 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -2,14 +2,14 @@
"actions": [],
"creation": "2014-10-16 14:35:41.950175",
"doctype": "DocType",
- "editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"details",
"operation",
"bom",
- "sequence_id",
+ "column_break_4",
"description",
+ "sequence_id",
"col_break1",
"completed_qty",
"status",
@@ -48,6 +48,7 @@
{
"fieldname": "bom",
"fieldtype": "Link",
+ "in_list_view": 1,
"label": "BOM",
"no_copy": 1,
"options": "BOM",
@@ -67,6 +68,7 @@
"fieldtype": "Column Break"
},
{
+ "columns": 1,
"description": "Operation completed for how many finished goods?",
"fieldname": "completed_qty",
"fieldtype": "Float",
@@ -76,6 +78,7 @@
"read_only": 1
},
{
+ "columns": 1,
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
@@ -118,6 +121,7 @@
"fieldtype": "Column Break"
},
{
+ "columns": 1,
"description": "in Minutes",
"fieldname": "time_in_mins",
"fieldtype": "Float",
@@ -195,12 +199,16 @@
"label": "Sequence ID",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-14 12:58:49.241252",
+ "modified": "2021-06-24 14:36:12.835543",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation",
@@ -209,4 +217,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py
index c6699bee48c..9b73aca6010 100644
--- a/erpnext/manufacturing/doctype/workstation/test_workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py
@@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
from __future__ import unicode_literals
+from erpnext.manufacturing.doctype.operation.test_operation import make_operation
import frappe
import unittest
from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError
+from erpnext.manufacturing.doctype.routing.test_routing import setup_bom, create_routing
+from frappe.test_runner import make_test_records
test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation')
+make_test_records('Workstation')
class TestWorkstation(unittest.TestCase):
-
def test_validate_timings(self):
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
@@ -21,6 +24,58 @@ class TestWorkstation(unittest.TestCase):
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
+ def test_update_bom_operation_rate(self):
+ operations = [
+ {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "hour_rate_rent": 300,
+ "time_in_mins": 60
+ },
+ {
+ "operation": "Test Operation B",
+ "workstation": "_Test Workstation B",
+ "hour_rate_rent": 1000,
+ "time_in_mins": 60
+ }
+ ]
+
+ for row in operations:
+ make_workstation(row)
+ make_operation(row)
+
+ test_routing_operations = [
+ {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "time_in_mins": 60
+ },
+ {
+ "operation": "Test Operation B",
+ "workstation": "_Test Workstation A",
+ "time_in_mins": 60
+ }
+ ]
+ routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations)
+ bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR")
+ w1 = frappe.get_doc("Workstation", "_Test Workstation A")
+ #resets values
+ w1.hour_rate_rent = 300
+ w1.hour_rate_labour = 0
+ w1.save()
+ bom_doc.update_cost()
+ bom_doc.reload()
+ self.assertEqual(w1.hour_rate, 300)
+ self.assertEqual(bom_doc.operations[0].hour_rate, 300)
+ w1.hour_rate_rent = 250
+ w1.save()
+ #updating after setting new rates in workstations
+ bom_doc.update_cost()
+ bom_doc.reload()
+ self.assertEqual(w1.hour_rate, 250)
+ self.assertEqual(bom_doc.operations[0].hour_rate, 250)
+ self.assertEqual(bom_doc.operations[1].hour_rate, 250)
+
def make_workstation(*args, **kwargs):
args = args if args else kwargs
if isinstance(args, tuple):
@@ -34,9 +89,10 @@ def make_workstation(*args, **kwargs):
"doctype": "Workstation",
"workstation_name": workstation_name
})
-
+ doc.hour_rate_rent = args.get("hour_rate_rent")
+ doc.hour_rate_labour = args.get("hour_rate_labour")
doc.insert()
return doc
except frappe.DuplicateEntryError:
- return frappe.get_doc("Workstation", workstation_name)
\ No newline at end of file
+ return frappe.get_doc("Workstation", workstation_name)
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index 3512e590458..f4483f75472 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -39,7 +39,8 @@ class Workstation(Document):
def update_bom_operation(self):
bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation`
- where workstation = %s""", self.name)
+ where workstation = %s and parenttype = 'routing' """, self.name)
+
for bom_no in bom_list:
frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s
where parent = %s and workstation = %s""",
@@ -71,7 +72,7 @@ def check_if_within_operating_hours(workstation, operation, from_datetime, to_da
def is_within_operating_hours(workstation, operation, from_datetime, to_datetime):
operation_length = time_diff_in_seconds(to_datetime, from_datetime)
workstation = frappe.get_doc("Workstation", workstation)
-
+
if not workstation.working_hours:
return
diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
index 48907adc5f3..858b5546b02 100644
--- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
+++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
@@ -20,17 +20,20 @@ def get_exploded_items(bom, data, indent=0, qty=1):
fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom'])
for item in exploded_items:
+ print(item.bom_no, indent)
item["indent"] = indent
data.append({
'item_code': item.item_code,
'item_name': item.item_name,
'indent': indent,
+ 'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level")
+ if item.bom_no else ""),
'bom': item.bom_no,
'qty': item.qty * qty,
'uom': item.uom,
'description': item.description,
'scrap': item.scrap
- })
+ })
if item.bom_no:
get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty)
@@ -68,6 +71,12 @@ def get_columns():
"fieldname": "uom",
"width": 100
},
+ {
+ "label": "BOM Level",
+ "fieldtype": "Data",
+ "fieldname": "bom_level",
+ "width": 100
+ },
{
"label": "Standard Description",
"fieldtype": "data",
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
new file mode 100644
index 00000000000..97e7e0a7d20
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
@@ -0,0 +1,105 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Cost of Poor Quality Report"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __("From Date"),
+ fieldname:"from_date",
+ fieldtype: "Datetime",
+ default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
+ reqd: 1
+ },
+ {
+ label: __("To Date"),
+ fieldname:"to_date",
+ fieldtype: "Datetime",
+ default: frappe.datetime.now_datetime(),
+ reqd: 1,
+ },
+ {
+ label: __("Job Card"),
+ fieldname: "name",
+ fieldtype: "Link",
+ options: "Job Card",
+ get_query: function() {
+ return {
+ filters: {
+ is_corrective_job_card: 1,
+ docstatus: 1
+ }
+ }
+ }
+ },
+ {
+ label: __("Work Order"),
+ fieldname: "work_order",
+ fieldtype: "Link",
+ options: "Work Order"
+ },
+ {
+ label: __("Operation"),
+ fieldname: "operation",
+ fieldtype: "Link",
+ options: "Operation",
+ get_query: function() {
+ return {
+ filters: {
+ is_corrective_operation: 1
+ }
+ }
+ }
+ },
+ {
+ label: __("Workstation"),
+ fieldname: "workstation",
+ fieldtype: "Link",
+ options: "Workstation"
+ },
+ {
+ label: __("Item"),
+ fieldname: "production_item",
+ fieldtype: "Link",
+ options: "Item"
+ },
+ {
+ label: __("Serial No"),
+ fieldname: "serial_no",
+ fieldtype: "Link",
+ options: "Serial No",
+ depends_on: "eval: doc.production_item",
+ get_query: function() {
+ var item_code = frappe.query_report.get_filter_value('production_item');
+ return {
+ filters: {
+ item_code: item_code
+ }
+ }
+ }
+ },
+ {
+ label: __("Batch No"),
+ fieldname: "batch_no",
+ fieldtype: "Link",
+ options: "Batch No",
+ depends_on: "eval: doc.production_item",
+ get_query: function() {
+ var item_code = frappe.query_report.get_filter_value('production_item');
+ return {
+ filters: {
+ item: item_code
+ }
+ }
+ }
+ },
+ ]
+};
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json
new file mode 100644
index 00000000000..ee63bc1c287
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json
@@ -0,0 +1,33 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-01-11 11:10:58.292896",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "modified": "2021-01-11 11:11:03.594242",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Cost of Poor Quality Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Job Card",
+ "report_name": "Cost of Poor Quality Report",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Manufacturing User"
+ },
+ {
+ "role": "Manufacturing Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
new file mode 100644
index 00000000000..9f81e7d26a1
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.utils import flt
+
+def execute(filters=None):
+ columns, data = [], []
+
+ columns = get_columns(filters)
+ data = get_data(filters)
+
+ return columns, data
+
+def get_data(report_filters):
+ data = []
+ operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
+ if operations:
+ operations = [d.name for d in operations]
+ fields = ["production_item as item_code", "item_name", "work_order", "operation",
+ "workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
+
+ filters = get_filters(report_filters, operations)
+
+ job_cards = frappe.get_all("Job Card", fields = fields,
+ filters = filters)
+
+ for row in job_cards:
+ row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
+ update_raw_material_cost(row, report_filters)
+ data.append(row)
+
+ return data
+
+def get_filters(report_filters, operations):
+ filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
+ for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
+ if report_filters.get(field):
+ if field != 'serial_no':
+ filters[field] = report_filters.get(field)
+ else:
+ filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
+
+ return filters
+
+def update_raw_material_cost(row, filters):
+ row.rm_cost = 0.0
+ for data in frappe.get_all("Job Card Item", fields = ["amount"],
+ filters={"parent": row.name, "docstatus": 1}):
+ row.rm_cost += data.amount
+
+def get_columns(filters):
+ return [
+ {
+ "label": _("Job Card"),
+ "fieldtype": "Link",
+ "fieldname": "name",
+ "options": "Job Card",
+ "width": "100"
+ },
+ {
+ "label": _("Work Order"),
+ "fieldtype": "Link",
+ "fieldname": "work_order",
+ "options": "Work Order",
+ "width": "100"
+ },
+ {
+ "label": _("Item Code"),
+ "fieldtype": "Link",
+ "fieldname": "item_code",
+ "options": "Item",
+ "width": "100"
+ },
+ {
+ "label": _("Item Name"),
+ "fieldtype": "Data",
+ "fieldname": "item_name",
+ "width": "100"
+ },
+ {
+ "label": _("Operation"),
+ "fieldtype": "Link",
+ "fieldname": "operation",
+ "options": "Operation",
+ "width": "100"
+ },
+ {
+ "label": _("Serial No"),
+ "fieldtype": "Data",
+ "fieldname": "serial_no",
+ "width": "100"
+ },
+ {
+ "label": _("Batch No"),
+ "fieldtype": "Data",
+ "fieldname": "batch_no",
+ "width": "100"
+ },
+ {
+ "label": _("Workstation"),
+ "fieldtype": "Link",
+ "fieldname": "workstation",
+ "options": "Workstation",
+ "width": "100"
+ },
+ {
+ "label": _("Operating Cost"),
+ "fieldtype": "Currency",
+ "fieldname": "operating_cost",
+ "width": "100"
+ },
+ {
+ "label": _("Raw Material Cost"),
+ "fieldtype": "Currency",
+ "fieldname": "rm_cost",
+ "width": "100"
+ },
+ {
+ "label": _("Total Time (in Mins)"),
+ "fieldtype": "Float",
+ "fieldname": "total_time_in_mins",
+ "width": "100"
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
index bd68db190e7..cb771e49941 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
@@ -68,6 +68,18 @@ frappe.query_reports["Job Card Summary"] = {
get_data: function(txt) {
return frappe.db.get_link_options('Item', txt);
}
+ },
+ {
+ label: __("Workstation"),
+ fieldname: "workstation",
+ fieldtype: "Link",
+ options: "Workstation"
+ },
+ {
+ label: __("Operation"),
+ fieldname: "operation",
+ fieldtype: "Link",
+ options: "Operation"
}
]
};
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json
index 9f08fc34cb8..ecf2b74bbed 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json
@@ -1,14 +1,16 @@
{
- "add_total_row": 0,
+ "add_total_row": 1,
+ "columns": [],
"creation": "2020-04-20 12:00:21.436619",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
+ "filters": [],
"idx": 0,
"is_standard": "Yes",
- "letter_head": "Gadgets International",
- "modified": "2020-04-20 12:00:21.436619",
+ "letter_head": "",
+ "modified": "2020-12-30 11:49:21.713561",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Summary",
diff --git a/erpnext/manufacturing/report/production_plan_summary/__init__.py b/erpnext/manufacturing/report/production_plan_summary/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js
new file mode 100644
index 00000000000..59396fef16e
--- /dev/null
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js
@@ -0,0 +1,32 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Production Plan Summary"] = {
+ "filters": [
+ {
+ fieldname: "production_plan",
+ label: __("Production Plan"),
+ fieldtype: "Link",
+ options: "Production Plan",
+ reqd: 1,
+ get_query: function() {
+ return {
+ filters: {
+ "docstatus": 1
+ }
+ };
+ }
+ }
+ ],
+ "formatter": function(value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname == "document_name") {
+ var color = data.pending_qty > 0 ? 'red': 'green';
+ value = `${data['document_name']}`;
+ }
+
+ return value;
+ },
+};
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json
new file mode 100644
index 00000000000..33aca21a6ea
--- /dev/null
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2020-12-27 11:43:39.781793",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-12-27 11:43:42.677584",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Production Plan Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Production Plan",
+ "report_name": "Production Plan Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Manufacturing User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
new file mode 100644
index 00000000000..81b1791ae81
--- /dev/null
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
@@ -0,0 +1,136 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import flt
+
+def execute(filters=None):
+ columns, data = [], []
+ data = get_data(filters)
+ columns = get_column(filters)
+
+ return columns, data
+
+def get_data(filters):
+ data = []
+
+ order_details = {}
+ get_work_order_details(filters, order_details)
+ get_purchase_order_details(filters, order_details)
+ get_production_plan_item_details(filters, data, order_details)
+
+ return data
+
+def get_production_plan_item_details(filters, data, order_details):
+ itemwise_indent = {}
+
+ production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
+ for row in production_plan_doc.po_items:
+ work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name,
+ "bom_no": row.bom_no, "production_item": row.item_code}, "name")
+
+ if row.item_code not in itemwise_indent:
+ itemwise_indent.setdefault(row.item_code, {})
+
+ data.append({
+ "indent": 0,
+ "item_code": row.item_code,
+ "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
+ "qty": row.planned_qty,
+ "document_type": "Work Order",
+ "document_name": work_order,
+ "bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
+ "produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"),
+ "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty"))
+ })
+
+ get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
+
+def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details):
+ for item in production_plan_doc.sub_assembly_items:
+ if row.name == item.production_plan_item:
+ subcontracted_item = (item.type_of_manufacturing == 'Subcontract')
+
+ if subcontracted_item:
+ docname = frappe.get_cached_value("Purchase Order Item",
+ {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent")
+ else:
+ docname = frappe.get_cached_value("Work Order",
+ {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name")
+
+ data.append({
+ "indent": 1,
+ "item_code": item.production_item,
+ "item_name": item.item_name,
+ "qty": item.qty,
+ "document_type": "Work Order" if not subcontracted_item else "Purchase Order",
+ "document_name": docname,
+ "bom_level": item.bom_level,
+ "produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"),
+ "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty"))
+ })
+
+def get_work_order_details(filters, order_details):
+ for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")},
+ fields=["name", "produced_qty", "production_plan", "production_item"]):
+ order_details.setdefault((row.name, row.production_item), row)
+
+def get_purchase_order_details(filters, order_details):
+ for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")},
+ fields=["parent", "received_qty as produced_qty", "item_code"]):
+ order_details.setdefault((row.parent, row.item_code), row)
+
+def get_column(filters):
+ return [
+ {
+ "label": "Finished Good",
+ "fieldtype": "Link",
+ "fieldname": "item_code",
+ "width": 300,
+ "options": "Item"
+ },
+ {
+ "label": "Item Name",
+ "fieldtype": "data",
+ "fieldname": "item_name",
+ "width": 100
+ },
+ {
+ "label": "Document Type",
+ "fieldtype": "Link",
+ "fieldname": "document_type",
+ "width": 150,
+ "options": "DocType"
+ },
+ {
+ "label": "Document Name",
+ "fieldtype": "Dynamic Link",
+ "fieldname": "document_name",
+ "width": 150
+ },
+ {
+ "label": "BOM Level",
+ "fieldtype": "Int",
+ "fieldname": "bom_level",
+ "width": 100
+ },
+ {
+ "label": "Order Qty",
+ "fieldtype": "Float",
+ "fieldname": "qty",
+ "width": 120
+ },
+ {
+ "label": "Received Qty",
+ "fieldtype": "Float",
+ "fieldname": "produced_qty",
+ "width": 160
+ },
+ {
+ "label": "Pending Qty",
+ "fieldtype": "Float",
+ "fieldname": "pending_qty",
+ "width": 110
+ }
+ ]
diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
index fb047b230ce..612dad0bf51 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
@@ -19,7 +19,7 @@ def execute(filters=None):
return columns, data, None, chart_data
def get_data(filters):
- query_filters = {"docstatus": 1}
+ query_filters = {"docstatus": ("<", 2)}
fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty",
"planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"]
@@ -62,7 +62,8 @@ def get_chart_based_on_status(data):
"Not Started": 0,
"In Process": 0,
"Stopped": 0,
- "Completed": 0
+ "Completed": 0,
+ "Draft": 0
}
for d in data:
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index ed6fefdd879..29376f00a1c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -288,3 +288,6 @@ execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
+erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
+erpnext.patches.v13_0.update_job_card_details
+erpnext.patches.v13_0.update_level_in_bom #1234sswef
diff --git a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py
new file mode 100644
index 00000000000..be85cfdeeff
--- /dev/null
+++ b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py
@@ -0,0 +1,8 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doctype("Buying Settings")
+ buying_settings = frappe.get_single("Buying Settings")
+ buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0
+ buying_settings.save()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_job_card_details.py b/erpnext/patches/v13_0/update_job_card_details.py
new file mode 100644
index 00000000000..d4e65c6f2f2
--- /dev/null
+++ b/erpnext/patches/v13_0/update_job_card_details.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("manufacturing", "doctype", "job_card")
+ frappe.reload_doc("manufacturing", "doctype", "job_card_item")
+ frappe.reload_doc("manufacturing", "doctype", "work_order_operation")
+
+ frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo
+ SET jc.hour_rate = wo.hour_rate
+ WHERE
+ jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0
+ """)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_level_in_bom.py b/erpnext/patches/v13_0/update_level_in_bom.py
new file mode 100644
index 00000000000..0d03c42e980
--- /dev/null
+++ b/erpnext/patches/v13_0/update_level_in_bom.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2020, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ for document in ["bom", "bom_item", "bom_explosion_item"]:
+ frappe.reload_doc('manufacturing', 'doctype', document)
+
+ frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1")
+
+ bom_list = frappe.db.sql_list("""select name from `tabBOM` bom
+ where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item`
+ where parent=bom.name and ifnull(bom_no, '')!='')""")
+
+ count = 0
+ while(count < len(bom_list)):
+ for parent_bom in get_parent_boms(bom_list[count]):
+ bom_doc = frappe.get_cached_doc("BOM", parent_bom)
+ bom_doc.set_bom_level(update=True)
+ bom_list.append(parent_bom)
+ count += 1
+
+def get_parent_boms(bom_no):
+ return frappe.db.sql_list("""
+ select distinct bom_item.parent from `tabBOM Item` bom_item
+ where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
+ and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
+ """, bom_no)
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js
index d1ed91fac70..24ffce537c1 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.js
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js
@@ -12,8 +12,12 @@ frappe.ui.form.on('Additional Salary', {
}
};
});
+ },
- frm.trigger('set_earning_component');
+ onload: function(frm) {
+ if (frm.doc.type) {
+ frm.trigger('set_component_query');
+ }
},
employee: function(frm) {
@@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', {
},
company: function(frm) {
- frm.trigger('set_earning_component');
+ frm.set_value("type", "");
+ frm.trigger('set_component_query');
},
- set_earning_component: function(frm) {
+ set_component_query: function(frm) {
if (!frm.doc.company) return;
+ let filters = {company: frm.doc.company};
+ if (frm.doc.type) {
+ filters.type = frm.doc.type;
+ }
frm.set_query("salary_component", function() {
return {
- filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company}
+ filters: filters
};
});
},
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index f2892600d12..496c37b2fad 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', {
});
frm.set_query('employee', 'employees', () => {
- if (!frm.doc.company) {
- frappe.msgprint(__("Please set a Company"));
- return [];
+ let error_fields = [];
+ let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date'];
+
+ let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]);
+
+ mandatory_fields.forEach(field => {
+ if (!frm.doc[field]) {
+ error_fields.push(frappe.unscrub(field));
+ }
+ });
+
+ if (error_fields && error_fields.length) {
+ message = message + '
- -
+-
{{ _("No tasks") }}
-{% endif %} + {{ progress_bar(doc.percent_complete) }} + {% if doc.tasks %} +{{ _("No Tasks") }}
+ {% endif %} - + {% if doc.timesheets %} +{{ _("No Timesheets") }}
+ {% endif %} -- {% endif %} -{% else %} -
{{ _("No time sheets") }}
-{% endif %} - -{% if doc.attachments %} - - -