diff --git a/.flake8 b/.flake8 index 56c9b9a3699..5735456ae7d 100644 --- a/.flake8 +++ b/.flake8 @@ -28,6 +28,7 @@ ignore = B007, B950, W191, + E124, # closing bracket, irritating while writing QB code max-line-length = 200 exclude=.github/helper/semgrep_rules diff --git a/.github/labeler.yml b/.github/labeler.yml index fc3f06da926..3aaba71b124 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,53 +1,53 @@ accounts: -- 'erpnext/accounts/*' -- 'erpnext/controllers/accounts_controller.py' -- 'erpnext/controllers/taxes_and_totals.py' +- erpnext/accounts/* +- erpnext/controllers/accounts_controller.py +- erpnext/controllers/taxes_and_totals.py stock: -- 'erpnext/stock/*' -- 'erpnext/controllers/stock_controller.py' -- 'erpnext/controllers/item_variant.py' +- erpnext/stock/* +- erpnext/controllers/stock_controller.py +- erpnext/controllers/item_variant.py assets: -- 'erpnext/assets/*' +- erpnext/assets/* regional: -- 'erpnext/regional/*' +- erpnext/regional/* selling: -- 'erpnext/selling/*' -- 'erpnext/controllers/selling_controller.py' +- erpnext/selling/* +- erpnext/controllers/selling_controller.py buying: -- 'erpnext/buying/*' -- 'erpnext/controllers/buying_controller.py' +- erpnext/buying/* +- erpnext/controllers/buying_controller.py support: -- 'erpnext/support/*' +- erpnext/support/* POS: -- 'pos*' +- pos* ecommerce: -- 'erpnext/e_commerce/*' +- erpnext/e_commerce/* maintenance: -- 'erpnext/maintenance/*' +- erpnext/maintenance/* manufacturing: -- 'erpnext/manufacturing/*' +- erpnext/manufacturing/* crm: -- 'erpnext/crm/*' +- erpnext/crm/* HR: -- 'erpnext/hr/*' +- erpnext/hr/* payroll: -- 'erpnext/payroll*' +- erpnext/payroll* projects: -- 'erpnext/projects/*' +- erpnext/projects/* # Any python files modifed but no test files modified needs-tests: diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 22c81ddd463..9e2cdfffd9a 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -254,11 +254,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): enable_check = "enable_deferred_revenue" \ if doc.doctype=="Sales Invoice" else "enable_deferred_expense" + accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto') + def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on): start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date) if not (start_date and end_date): return - account_currency = get_account_currency(item.expense_account) + account_currency = get_account_currency(item.expense_account or item.income_account) if doc.doctype == "Sales Invoice": against, project = doc.customer, doc.project credit_account, debit_account = item.income_account, item.deferred_revenue_account @@ -279,6 +281,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): if not amount: return + # check if books nor frozen till endate: + if getdate(end_date) >= getdate(accounts_frozen_upto): + end_date = get_last_day(add_days(accounts_frozen_upto, 1)) + 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) @@ -406,8 +412,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, 'account': credit_account, 'credit': base_amount, 'credit_in_account_currency': amount, - 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier', - 'party': against, 'account_currency': account_currency, 'reference_name': doc.name, 'reference_type': doc.doctype, @@ -420,8 +424,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, 'account': debit_account, 'debit': base_amount, 'debit_in_account_currency': amount, - 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier', - 'party': against, 'account_currency': account_currency, 'reference_name': doc.name, 'reference_type': doc.doctype, 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 e786d13c95d..1403303f53c 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html from openpyxl.styles import Font from openpyxl.utils import get_column_letter +INVALID_VALUES = ("", None) class BankStatementImport(DataImport): def __init__(self, *args, **kwargs): @@ -95,6 +96,18 @@ def download_errored_template(data_import_name): data_import = frappe.get_doc("Bank Statement Import", data_import_name) data_import.export_errored_rows() +def parse_data_from_template(raw_data): + data = [] + + for i, row in enumerate(raw_data): + if all(v in INVALID_VALUES for v in row): + # empty row + continue + + data.append(row) + + return data + def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options): """This method runs in background job""" @@ -104,7 +117,8 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url, file = import_file_path if import_file_path else google_sheets_url import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records") - data = import_file.raw_data + + data = parse_data_from_template(import_file.raw_data) if import_file_path: add_bank_account(data, bank_account) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 44cea31ed38..51e1d6e9a00 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -2,9 +2,10 @@ # For license information, please see license.txt +from functools import reduce + import frappe from frappe.utils import flt -from six.moves import reduce from erpnext.controllers.status_updater import StatusUpdater diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 8fc4e8c5e3a..ac8ab310240 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -407,13 +407,14 @@ class JournalEntry(AccountsController): debit_or_credit = 'Debit' if d.debit else 'Credit' party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no, debit_or_credit) + against_voucher = ['', against_voucher[1]] else: if d.reference_type == "Sales Invoice": party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1] else: party_account = against_voucher[1] - if (against_voucher[0] != d.party or party_account != d.account): + if (against_voucher[0] != cstr(d.party) or party_account != d.account): frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}") .format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1], d.reference_type, d.reference_name)) @@ -478,13 +479,22 @@ class JournalEntry(AccountsController): def set_against_account(self): accounts_debited, accounts_credited = [], [] - for d in self.get("accounts"): - if flt(d.debit > 0): accounts_debited.append(d.party or d.account) - if flt(d.credit) > 0: accounts_credited.append(d.party or d.account) + if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'): + for d in self.get('accounts'): + if d.reference_type == 'Sales Invoice': + field = 'customer' + else: + field = 'supplier' - for d in self.get("accounts"): - if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) - if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) + d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field) + else: + for d in self.get("accounts"): + if flt(d.debit > 0): accounts_debited.append(d.party or d.account) + if flt(d.credit) > 0: accounts_credited.append(d.party or d.account) + + for d in self.get("accounts"): + if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) + if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) def validate_debit_credit_amount(self): for d in self.get('accounts'): diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index ddb833ff3bd..19d8d49dfea 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -135,7 +135,7 @@ class OpeningInvoiceCreationTool(Document): default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos") rate = flt(row.outstanding_amount) / flt(row.qty) - return frappe._dict({ + item_dict = frappe._dict({ "uom": default_uom, "rate": rate or 0.0, "qty": row.qty, @@ -146,6 +146,13 @@ class OpeningInvoiceCreationTool(Document): "cost_center": cost_center }) + for dimension in get_accounting_dimensions(): + item_dict.update({ + dimension: row.get(dimension) + }) + + return item_dict + item = get_item_dict() invoice = frappe._dict({ @@ -166,7 +173,7 @@ class OpeningInvoiceCreationTool(Document): accounting_dimension = get_accounting_dimensions() for dimension in accounting_dimension: invoice.update({ - dimension: item.get(dimension) + dimension: self.get(dimension) or item.get(dimension) }) return invoice diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index b5aae9845b6..6700e9b975d 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -7,21 +7,26 @@ import frappe from frappe.cache_manager import clear_doctype_cache from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( + create_dimension, + disable_dimension, +) from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) -test_dependencies = ["Customer", "Supplier"] +test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] class TestOpeningInvoiceCreationTool(unittest.TestCase): def setUp(self): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): make_company() + create_dimension() - def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None): + def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None): doc = frappe.get_single("Opening Invoice Creation Tool") args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company, - party_1=party_1, party_2=party_2, invoice_number=invoice_number) + party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department) doc.update(args) return doc.make_invoices() @@ -106,6 +111,19 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): doc = frappe.get_doc('Sales Invoice', inv) doc.cancel() + def test_opening_invoice_with_accounting_dimension(self): + invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC') + + expected_value = { + "keys": ["customer", "outstanding_amount", "status", "department"], + 0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"], + 1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"], + } + self.check_expected_values(invoices, expected_value, invoice_type="Sales") + + def tearDown(self): + disable_dimension() + def get_opening_invoice_creation_dict(**args): party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" company = args.get("company", "_Test Company") diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0e07abd7255..02a144d3e79 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -3,6 +3,7 @@ import json +from functools import reduce import frappe from frappe import ValidationError, _, scrub, throw @@ -1523,6 +1524,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.received_amount = received_amount pe.letter_head = doc.get("letter_head") + if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']: + pe.project = (doc.get('project') or + reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items + if pe.party_type in ["Customer", "Supplier"]: bank_account = get_party_bank_account(pe.party_type, pe.party) pe.set("bank_account", bank_account) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 134bccf3d1a..f66abdc66bc 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -158,6 +158,20 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.") .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) + def validate_invalid_serial_nos(self, item): + serial_nos = get_serial_nos(item.serial_no) + error_msg = [] + invalid_serials, msg = "", "" + for serial_no in serial_nos: + if not frappe.db.exists('Serial No', serial_no): + invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no + msg = (_("Row #{}: Following Serial numbers for item {} are Invalid: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials))) + if invalid_serials: + error_msg.append(msg) + + if error_msg: + frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) + def validate_stock_availablility(self): if self.is_return or self.docstatus != 1: return @@ -167,6 +181,7 @@ class POSInvoice(SalesInvoice): if d.serial_no: self.validate_pos_reserved_serial_nos(d) self.validate_delivered_serial_nos(d) + self.validate_invalid_serial_nos(d) elif d.batch_no: self.validate_pos_reserved_batch_qty(d) else: diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 56479a0b77d..ba751c081bb 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -354,6 +354,24 @@ class TestPOSInvoice(unittest.TestCase): pos2.insert() self.assertRaises(frappe.ValidationError, pos2.submit) + def test_invalid_serial_no_validation(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + se = make_serialized_item(company='_Test Company', + target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC') + serial_nos = se.get("items")[0].serial_no + 'wrong' + + pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', + account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', + expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', + item=se.get("items")[0].item_code, rate=1000, qty=2, do_not_save=1) + + pos.get('items')[0].has_serial_no = 1 + pos.get('items')[0].serial_no = serial_nos + pos.insert() + + self.assertRaises(frappe.ValidationError, pos.submit) + def test_loyalty_points(self): from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 5746a840f30..968137e7f82 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -628,6 +628,26 @@ class TestPricingRule(unittest.TestCase): for doc in [si, si1]: doc.delete() + def test_multiple_pricing_rules_with_min_qty(self): + make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4, + apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1") + make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4, + apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2") + + si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD") + item = si.items[0] + item.stock_qty = 1 + si.save() + self.assertFalse(item.discount_percentage) + item.qty = 5 + item.stock_qty = 5 + si.save() + self.assertEqual(item.discount_percentage, 30) + si.delete() + + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") + test_dependencies = ["Campaign"] def make_pricing_rule(**args): diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 02bfc9defd7..7792590c9c7 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -73,7 +73,7 @@ def sorted_by_priority(pricing_rules, args, doc=None): for key in sorted(pricing_rule_dict): pricing_rules_list.extend(pricing_rule_dict.get(key)) - return pricing_rules_list or pricing_rules + return pricing_rules_list def filter_pricing_rule_based_on_condition(pricing_rules, doc=None): filtered_pricing_rules = [] diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index 6336db16ebc..f54bce8aac7 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -1,6 +1,8 @@ {% include "erpnext/regional/india/taxes.js" %} +{% include "erpnext/regional/india/e_invoice/einvoice.js" %} erpnext.setup_auto_gst_taxation('Sales Invoice'); +erpnext.setup_einvoice_actions('Sales Invoice') frappe.ui.form.on("Sales Invoice", { setup: function(frm) { diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js index d9d6634c397..f01325d80bd 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js @@ -36,4 +36,139 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) { }; list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false); + + const generate_irns = () => { + const docnames = list_view.get_checked_items(true); + if (docnames && docnames.length) { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices', + args: { docnames }, + freeze: true, + freeze_message: __('Generating E-Invoices...') + }); + } else { + frappe.msgprint({ + message: __('Please select at least one sales invoice to generate IRN'), + title: __('No Invoice Selected'), + indicator: 'red' + }); + } + }; + + const cancel_irns = () => { + const docnames = list_view.get_checked_items(true); + + const fields = [ + { + "label": "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; + + const d = new frappe.ui.Dialog({ + title: __("Cancel IRN"), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_irns', + args: { + doctype: list_view.doctype, + docnames, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + freeze_message: __('Cancelling E-Invoices...'), + }); + d.hide(); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + + let einvoicing_enabled = false; + frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => { + einvoicing_enabled = enabled; + }); + + list_view.$result.on("change", "input[type=checkbox]", () => { + if (einvoicing_enabled) { + const docnames = list_view.get_checked_items(true); + // show/hide e-invoicing actions when no sales invoices are checked + if (docnames && docnames.length) { + // prevent adding actions twice if e-invoicing action group already exists + if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) { + list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing')); + list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing')); + } + } else { + list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing')); + list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing')); + } + } + }); + + frappe.realtime.on("bulk_einvoice_generation_complete", (data) => { + const { failures, user, invoices } = data; + + if (invoices.length != failures.length) { + frappe.msgprint({ + message: __('{0} e-invoices generated successfully', [invoices.length]), + title: __('Bulk E-Invoice Generation Complete'), + indicator: 'orange' + }); + } + + if (failures && failures.length && user == frappe.session.user) { + let message = ` + Failed to generate IRNs for following ${failures.length} sales invoices: + + `; + frappe.msgprint({ + message: message, + title: __('Bulk E-Invoice Generation Complete'), + indicator: 'orange' + }); + } + }); + + frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => { + const { failures, user, invoices } = data; + + if (invoices.length != failures.length) { + frappe.msgprint({ + message: __('{0} e-invoices cancelled successfully', [invoices.length]), + title: __('Bulk E-Invoice Cancellation Complete'), + indicator: 'orange' + }); + } + + if (failures && failures.length && user == frappe.session.user) { + let message = ` + Failed to cancel IRNs for following ${failures.length} sales invoices: + + `; + frappe.msgprint({ + message: message, + title: __('Bulk E-Invoice Cancellation Complete'), + indicator: 'orange' + }); + } + }); }; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 39dfd8d76d5..af6a52a6429 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -469,7 +469,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e let row = frappe.get_doc(d.doctype, d.name) set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail) }); - frm.trigger("calculate_timesheet_totals"); + this.frm.trigger("calculate_timesheet_totals"); } } }; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 98bc9539c2f..bc443581e45 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -43,6 +43,7 @@ from erpnext.setup.doctype.company.company import update_company_current_month_s from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos +from erpnext.stock.utils import calculate_mapped_packed_items_return form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -293,6 +294,8 @@ class SalesInvoice(SellingController): def before_cancel(self): self.check_if_consolidated_invoice() + + super(SalesInvoice, self).before_cancel() self.update_time_sheet(None) def on_cancel(self): @@ -728,8 +731,11 @@ class SalesInvoice(SellingController): def update_packing_list(self): if cint(self.update_stock) == 1: - from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - make_packing_list(self) + if cint(self.is_return) and self.return_against: + calculate_mapped_packed_items_return(self) + else: + from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) else: self.set('packed_items', []) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index c02c80a0fd9..941061f2a22 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1781,47 +1781,6 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, "2019-01-30") - def test_deferred_revenue_post_account_freeze_upto_by_admin(self): - frappe.set_user("Administrator") - - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) - frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None) - - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") - - item = create_item("_Test Item for Deferred Accounting") - item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_account - item.no_of_months = 12 - item.save() - - si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True) - si.items[0].enable_deferred_revenue = 1 - si.items[0].service_start_date = "2019-01-10" - si.items[0].service_end_date = "2019-03-15" - si.items[0].deferred_revenue_account = deferred_account - si.save() - si.submit() - - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31')) - frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager') - - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) - - pda1.insert() - self.assertRaises(frappe.ValidationError, pda1.submit) - - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) - frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None) - def test_fixed_deferred_revenue(self): deferred_account = create_account(account_name="Deferred Revenue", parent_account="Current Liabilities - _TC", company="_Test Company") @@ -2141,6 +2100,54 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(data['billLists'][0]['actualFromStateCode'],7) self.assertEqual(data['billLists'][0]['fromStateCode'],27) + def test_einvoice_submission_without_irn(self): + # init + einvoice_settings = frappe.get_doc('E Invoice Settings') + einvoice_settings.enable = 1 + einvoice_settings.applicable_from = nowdate() + einvoice_settings.append('credentials', { + 'company': '_Test Company', + 'gstin': '27AAECE4835E1ZR', + 'username': 'test', + 'password': 'test' + }) + einvoice_settings.save() + + country = frappe.flags.country + frappe.flags.country = 'India' + + si = make_sales_invoice_for_ewaybill() + self.assertRaises(frappe.ValidationError, si.submit) + + si.irn = 'test_irn' + si.submit() + + # reset + einvoice_settings = frappe.get_doc('E Invoice Settings') + einvoice_settings.enable = 0 + frappe.flags.country = country + + def test_einvoice_json(self): + from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals + + si = get_sales_invoice_for_e_invoice() + si.discount_amount = 100 + si.save() + + einvoice = make_einvoice(si) + self.assertTrue(einvoice['EwbDtls']) + validate_totals(einvoice) + + si.apply_discount_on = 'Net Total' + si.save() + einvoice = make_einvoice(si) + validate_totals(einvoice) + + [d.set('included_in_print_rate', 1) for d in si.taxes] + si.save() + einvoice = make_einvoice(si) + validate_totals(einvoice) + def test_item_tax_net_range(self): item = create_item("T Shirt") @@ -2233,9 +2240,9 @@ class TestSalesInvoice(unittest.TestCase): asset.load_from_db() expected_values = [ - ["2020-06-30", 1311.48, 1311.48], - ["2021-06-30", 20000.0, 21311.48], - ["2021-09-30", 5041.1, 26352.58] + ["2020-06-30", 1366.12, 1366.12], + ["2021-06-30", 20000.0, 21366.12], + ["2021-09-30", 5041.1, 26407.22] ] for i, schedule in enumerate(asset.schedules): @@ -2283,12 +2290,12 @@ class TestSalesInvoice(unittest.TestCase): asset.load_from_db() expected_values = [ - ["2020-06-30", 1311.48, 1311.48, True], - ["2021-06-30", 20000.0, 21311.48, True], - ["2022-06-30", 20000.0, 41311.48, False], - ["2023-06-30", 20000.0, 61311.48, False], - ["2024-06-30", 20000.0, 81311.48, False], - ["2025-06-06", 18688.52, 100000.0, False] + ["2020-06-30", 1366.12, 1366.12, True], + ["2021-06-30", 20000.0, 21366.12, True], + ["2022-06-30", 20000.0, 41366.12, False], + ["2023-06-30", 20000.0, 61366.12, False], + ["2024-06-30", 20000.0, 81366.12, False], + ["2025-06-06", 18633.88, 100000.0, False] ] for i, schedule in enumerate(asset.schedules): @@ -2482,6 +2489,74 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance) + def test_multi_currency_deferred_revenue_via_journal_entry(self): + deferred_account = create_account(account_name="Deferred Revenue", + parent_account="Current Liabilities - _TC", company="_Test Company") + + acc_settings = frappe.get_single('Accounts Settings') + acc_settings.book_deferred_entries_via_journal_entry = 1 + acc_settings.submit_journal_entries = 1 + acc_settings.save() + + item = create_item("_Test Item for Deferred Accounting") + item.enable_deferred_expense = 1 + item.deferred_revenue_account = deferred_account + item.save() + + si = create_sales_invoice(customer='_Test Customer USD', currency='USD', + item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True) + + si.set_posting_time = 1 + si.posting_date = '2019-01-01' + si.debit_to = '_Test Receivable USD - _TC' + si.items[0].enable_deferred_revenue = 1 + si.items[0].service_start_date = "2019-01-01" + si.items[0].service_end_date = "2019-03-30" + si.items[0].deferred_expense_account = deferred_account + si.save() + si.submit() + + frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31')) + + pda1 = frappe.get_doc(dict( + doctype='Process Deferred Accounting', + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company" + )) + + pda1.insert() + pda1.submit() + + expected_gle = [ + ["Sales - _TC", 0.0, 2089.89, "2019-01-28"], + [deferred_account, 2089.89, 0.0, "2019-01-28"], + ["Sales - _TC", 0.0, 1887.64, "2019-02-28"], + [deferred_account, 1887.64, 0.0, "2019-02-28"], + ["Sales - _TC", 0.0, 2022.47, "2019-03-15"], + [deferred_account, 2022.47, 0.0, "2019-03-15"] + ] + + gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + from `tabGL Entry` + where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s + order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.credit) + self.assertEqual(expected_gle[i][2], gle.debit) + self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + + acc_settings = frappe.get_single('Accounts Settings') + acc_settings.book_deferred_entries_via_journal_entry = 0 + acc_settings.submit_journal_entriessubmit_journal_entries = 0 + acc_settings.save() + + frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index 7e5129911e4..792e7d21a78 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -71,7 +71,8 @@ class ShippingRule(Document): if doc.currency != doc.company_currency: shipping_amount = flt(shipping_amount / doc.conversion_rate, 2) - self.add_shipping_rule_to_tax_table(doc, shipping_amount) + if shipping_amount: + self.add_shipping_rule_to_tax_table(doc, shipping_amount) def get_shipping_amount_from_rules(self, value): for condition in self.get("conditions"): diff --git a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json index d2c505c6300..e032bb307b0 100644 --- a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json +++ b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json @@ -28,14 +28,14 @@ { "columns": 2, "fieldname": "single_threshold", - "fieldtype": "Currency", + "fieldtype": "Float", "in_list_view": 1, "label": "Single Transaction Threshold" }, { "columns": 3, "fieldname": "cumulative_threshold", - "fieldtype": "Currency", + "fieldtype": "Float", "in_list_view": 1, "label": "Cumulative Transaction Threshold" }, @@ -59,7 +59,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-31 11:42:12.213977", + "modified": "2022-01-13 12:04:42.904263", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withholding Rate", @@ -68,5 +68,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json index 7de9ae15398..02e30c38356 100644 --- a/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json +++ b/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json @@ -2,15 +2,17 @@ "creation": "2021-08-24 12:28:18.044902", "docstatus": 0, "doctype": "Form Tour", + "first_document": 0, "idx": 0, + "include_name_field": 0, "is_standard": 1, - "modified": "2021-08-24 12:28:18.044902", + "modified": "2022-01-18 18:32:17.102330", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges Template", "owner": "Administrator", "reference_doctype": "Sales Taxes and Charges Template", - "save_on_complete": 0, + "save_on_complete": 1, "steps": [ { "description": "A name by which you will identify this template. You can change this later.", diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json index 2e0ab4305d0..aa7cdf788b0 100644 --- a/erpnext/accounts/module_onboarding/accounts/accounts.json +++ b/erpnext/accounts/module_onboarding/accounts/accounts.json @@ -13,15 +13,12 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts", "idx": 0, "is_complete": 0, - "modified": "2021-08-13 11:59:35.690443", + "modified": "2022-01-18 18:35:52.326688", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts", "owner": "Administrator", "steps": [ - { - "step": "Company" - }, { "step": "Chart of Accounts" }, diff --git a/erpnext/accounts/onboarding_step/company/company.json b/erpnext/accounts/onboarding_step/company/company.json deleted file mode 100644 index 4992e4d018b..00000000000 --- a/erpnext/accounts/onboarding_step/company/company.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "action": "Go to Page", - "action_label": "Let's Review your Company", - "creation": "2021-06-29 14:47:42.497318", - "description": "# Company\n\nIn ERPNext, you can also create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company. \n", - "docstatus": 0, - "doctype": "Onboarding Step", - "idx": 0, - "is_complete": 0, - "is_single": 0, - "is_skipped": 0, - "modified": "2021-08-13 11:43:35.767341", - "modified_by": "Administrator", - "name": "Company", - "owner": "Administrator", - "path": "app/company", - "reference_document": "Company", - "show_form_tour": 0, - "show_full_form": 0, - "title": "Review Company", - "validate_action": 1 -} \ No newline at end of file diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 6b4b43d30b7..c13bc23c159 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -58,7 +58,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) party = frappe.get_doc(party_type, party) - currency = party.default_currency if party.get("default_currency") else get_company_currency(company) + currency = party.get("default_currency") or currency or get_company_currency(company) party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address) set_contact_details(party_details, party, party_type) diff --git a/erpnext/demo/__init__.py b/erpnext/accounts/print_format/gst_e_invoice/__init__.py similarity index 100% rename from erpnext/demo/__init__.py rename to erpnext/accounts/print_format/gst_e_invoice/__init__.py diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html new file mode 100644 index 00000000000..e6580493095 --- /dev/null +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -0,0 +1,173 @@ +{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%} +{%- set einvoice = json.loads(doc.signed_einvoice) -%} + +
+
+ {% if letter_head and not no_letterhead %} +
{{ letter_head }}
+ {% endif %} + +
+ {% if print_settings.repeat_header_footer %} + + {% endif %} +
1. Transaction Details
+
+
+
+
+
{{ einvoice.Irn }}
+
+
+
+
{{ einvoice.AckNo }}
+
+
+
+
{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}
+
+
+
+
{{ einvoice.TranDtls.SupTyp }}
+
+
+
+
{{ einvoice.DocDtls.Typ }}
+
+
+
+
{{ einvoice.DocDtls.No }}
+
+
+
+ +
+
+
2. Party Details
+
+ {%- set seller = einvoice.SellerDtls -%} +
+
Seller
+

{{ seller.Gstin }}

+

{{ seller.LglNm }}

+

{{ seller.Addr1 }}

+ {%- if seller.Addr2 -%}

{{ seller.Addr2 }}

{% endif %} +

{{ seller.Loc }}

+

{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}

+ + {%- if einvoice.ShipDtls -%} + {%- set shipping = einvoice.ShipDtls -%} +
Shipped From
+

{{ shipping.Gstin }}

+

{{ shipping.LglNm }}

+

{{ shipping.Addr1 }}

+ {%- if shipping.Addr2 -%}

{{ shipping.Addr2 }}

{% endif %} +

{{ shipping.Loc }}

+

{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}

+ {% endif %} +
+ {%- set buyer = einvoice.BuyerDtls -%} +
+
Buyer
+

{{ buyer.Gstin }}

+

{{ buyer.LglNm }}

+

{{ buyer.Addr1 }}

+ {%- if buyer.Addr2 -%}

{{ buyer.Addr2 }}

{% endif %} +

{{ buyer.Loc }}

+

{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}

+ + {%- if einvoice.DispDtls -%} + {%- set dispatch = einvoice.DispDtls -%} +
Dispatched From
+ {%- if dispatch.Gstin -%}

{{ dispatch.Gstin }}

{% endif %} +

{{ dispatch.LglNm }}

+

{{ dispatch.Addr1 }}

+ {%- if dispatch.Addr2 -%}

{{ dispatch.Addr2 }}

{% endif %} +

{{ dispatch.Loc }}

+

{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}

+ {% endif %} +
+
+
+
3. Item Details
+ + + + + + + + + + + + + + + + + + {% for item in einvoice.ItemList %} + + + + + + + + + + + + + + {% endfor %} + +
Sr. No.ItemHSN CodeQtyUOMRateDiscountTaxable AmountTax RateOther ChargesTotal
{{ item.SlNo }}{{ item.PrdDesc }}{{ item.HsnCd }}{{ item.Qty }}{{ item.Unit }}{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}{{ item.GstRt + item.CesRt }} %{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}
+
+
+
4. Value Details
+ + + + + + + + + + + + + + + + + {%- set value_details = einvoice.ValDtls -%} + + + + + + + + + + + + + +
Taxable AmountCGSTSGSTIGSTCESSState CESSDiscountOther ChargesRound OffTotal Value
{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}
+
+
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json new file mode 100644 index 00000000000..1001199a092 --- /dev/null +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json @@ -0,0 +1,24 @@ +{ + "align_labels_right": 1, + "creation": "2020-10-10 18:01:21.032914", + "custom_format": 0, + "default_print_language": "en-US", + "disabled": 1, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "", + "idx": 0, + "line_breaks": 1, + "modified": "2020-10-23 19:54:40.634936", + "modified_by": "Administrator", + "module": "Accounts", + "name": "GST E-Invoice", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 1, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 4559fa94a4a..8e3bd8be1b9 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -5,7 +5,6 @@ import frappe from frappe import _, scrub from frappe.utils import cint, flt -from six import iteritems from erpnext.accounts.party import get_partywise_advanced_payment_amount from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport @@ -40,7 +39,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): if self.filters.show_gl_balance: gl_balance_map = get_gl_balance(self.filters.report_date) - for party, party_dict in iteritems(self.party_total): + for party, party_dict in self.party_total.items(): if party_dict.outstanding == 0: continue diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index 1de6fb68241..86eb2134fe8 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -17,10 +17,42 @@ from erpnext.stock.doctype.item.test_item import create_item class TestDeferredRevenueAndExpense(unittest.TestCase): @classmethod def setUpClass(self): - clear_old_entries() + clear_accounts_and_items() create_company() + self.maxDiff = None + + def clear_old_entries(self): + sinv = qb.DocType("Sales Invoice") + sinv_item = qb.DocType("Sales Invoice Item") + pinv = qb.DocType("Purchase Invoice") + pinv_item = qb.DocType("Purchase Invoice Item") + + # delete existing invoices with deferred items + deferred_invoices = ( + qb.from_(sinv) + .join(sinv_item) + .on(sinv.name == sinv_item.parent) + .select(sinv.name) + .where(sinv_item.enable_deferred_revenue == 1) + .run() + ) + if deferred_invoices: + qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run() + + deferred_invoices = ( + qb.from_(pinv) + .join(pinv_item) + .on(pinv.name == pinv_item.parent) + .select(pinv.name) + .where(pinv_item.enable_deferred_expense == 1) + .run() + ) + if deferred_invoices: + qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run() def test_deferred_revenue(self): + self.clear_old_entries() + # created deferred expense accounts, if not found deferred_revenue_account = create_account( account_name="Deferred Revenue", @@ -108,6 +140,8 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): self.assertEqual(report.period_total, expected) def test_deferred_expense(self): + self.clear_old_entries() + # created deferred expense accounts, if not found deferred_expense_account = create_account( account_name="Deferred Expense", @@ -198,6 +232,91 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): ] self.assertEqual(report.period_total, expected) + def test_zero_months(self): + self.clear_old_entries() + # created deferred expense accounts, if not found + deferred_revenue_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _CD", + company="_Test Company DR", + ) + + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + acc_settings.book_deferred_entries_based_on = "Months" + acc_settings.save() + + customer = frappe.new_doc("Customer") + customer.customer_name = "_Test Customer DR" + customer.type = "Individual" + customer.insert() + + item = create_item( + "_Test Internet Subscription", + is_stock_item=0, + warehouse="All Warehouses - _CD", + company="_Test Company DR", + ) + item.enable_deferred_revenue = 1 + item.deferred_revenue_account = deferred_revenue_account + item.no_of_months = 0 + item.save() + + si = create_sales_invoice( + item=item.name, + company="_Test Company DR", + customer="_Test Customer DR", + debit_to="Debtors - _CD", + posting_date="2021-05-01", + parent_cost_center="Main - _CD", + cost_center="Main - _CD", + do_not_submit=True, + rate=300, + price_list_rate=300, + ) + si.items[0].enable_deferred_revenue = 1 + si.items[0].deferred_revenue_account = deferred_revenue_account + si.items[0].income_account = "Sales - _CD" + si.save() + si.submit() + + pda = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2021-05-01", + end_date="2021-08-01", + type="Income", + company="_Test Company DR", + ) + ) + pda.insert() + pda.submit() + + # execute report + fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + self.filters = frappe._dict( + { + "company": frappe.defaults.get_user_default("Company"), + "filter_based_on": "Date Range", + "period_start_date": "2021-05-01", + "period_end_date": "2021-08-01", + "from_fiscal_year": fiscal_year.year, + "to_fiscal_year": fiscal_year.year, + "periodicity": "Monthly", + "type": "Revenue", + "with_upcoming_postings": False, + } + ) + + report = Deferred_Revenue_and_Expense_Report(filters=self.filters) + report.run() + expected = [ + {"key": "may_2021", "total": 300.0, "actual": 300.0}, + {"key": "jun_2021", "total": 0, "actual": 0}, + {"key": "jul_2021", "total": 0, "actual": 0}, + {"key": "aug_2021", "total": 0, "actual": 0}, + ] + self.assertEqual(report.period_total, expected) def create_company(): company = frappe.db.exists("Company", "_Test Company DR") @@ -209,15 +328,11 @@ def create_company(): company.insert() -def clear_old_entries(): +def clear_accounts_and_items(): item = qb.DocType("Item") account = qb.DocType("Account") customer = qb.DocType("Customer") supplier = qb.DocType("Supplier") - sinv = qb.DocType("Sales Invoice") - sinv_item = qb.DocType("Sales Invoice Item") - pinv = qb.DocType("Purchase Invoice") - pinv_item = qb.DocType("Purchase Invoice Item") qb.from_(account).delete().where( (account.account_name == "Deferred Revenue") @@ -228,26 +343,3 @@ def clear_old_entries(): ).run() qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run() qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run() - - # delete existing invoices with deferred items - deferred_invoices = ( - qb.from_(sinv) - .join(sinv_item) - .on(sinv.name == sinv_item.parent) - .select(sinv.name) - .where(sinv_item.enable_deferred_revenue == 1) - .run() - ) - if deferred_invoices: - qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run() - - deferred_invoices = ( - qb.from_(pinv) - .join(pinv_item) - .on(pinv.name == pinv_item.parent) - .select(pinv.name) - .where(pinv_item.enable_deferred_expense == 1) - .run() - ) - if deferred_invoices: - qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run() diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index 33d17488256..203ea20882f 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -5,7 +5,7 @@ "label": "Profit and Loss" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Accounts\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Profit and Loss\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chart of Accounts\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Invoice\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Invoice\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Journal Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Payment Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Accounts Receivable\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"General Ledger\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Trial Balance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounting Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"General Ledger\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounts Receivable\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounts Payable\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Financial Statements\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Multi Currency\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bank Statement\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Subscription Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Goods and Services Tax (GST India)\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Share Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Cost Center and Budgeting\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Opening and Closing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Taxes\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Profitability\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Goods and Services Tax (GST India)\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}}]", "creation": "2020-03-02 15:41:59.515192", "docstatus": 0, "doctype": "Workspace", @@ -230,6 +230,7 @@ "hidden": 0, "is_query_report": 0, "label": "Payment Reconciliation", + "link_count": 0, "link_to": "Payment Reconciliation", "link_type": "DocType", "onboard": 0, @@ -346,6 +347,7 @@ "hidden": 0, "is_query_report": 0, "label": "Payment Reconciliation", + "link_count": 0, "link_to": "Payment Reconciliation", "link_type": "DocType", "onboard": 0, @@ -527,16 +529,17 @@ "type": "Link" }, { - "dependencies": "GL Entry", - "hidden": 0, - "is_query_report": 1, - "label": "KSA VAT Report", - "link_to": "KSA VAT", - "link_type": "Report", - "onboard": 0, - "only_for": "Saudi Arabia", - "type": "Link" - }, + "dependencies": "GL Entry", + "hidden": 0, + "is_query_report": 1, + "label": "KSA VAT Report", + "link_count": 0, + "link_to": "KSA VAT", + "link_type": "Report", + "onboard": 0, + "only_for": "Saudi Arabia", + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -1158,15 +1161,16 @@ "type": "Link" }, { - "hidden": 0, - "is_query_report": 0, - "label": "KSA VAT Setting", - "link_to": "KSA VAT Setting", - "link_type": "DocType", - "onboard": 0, - "only_for": "Saudi Arabia", - "type": "Link" - }, + "hidden": 0, + "is_query_report": 0, + "label": "KSA VAT Setting", + "link_count": 0, + "link_to": "KSA VAT Setting", + "link_type": "DocType", + "onboard": 0, + "only_for": "Saudi Arabia", + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -1220,7 +1224,7 @@ "type": "Link" } ], - "modified": "2021-08-27 12:15:52.872471", + "modified": "2022-01-13 17:25:09.835345", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -1229,7 +1233,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 2, + "sequence_id": 2.0, "shortcuts": [ { "label": "Chart of Accounts", @@ -1278,4 +1282,4 @@ } ], "title": "Accounting" -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 153f5c537a2..f414930d722 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -108,6 +108,10 @@ frappe.ui.form.on('Asset', { frm.trigger("create_asset_repair"); }, __("Manage")); + frm.add_custom_button(__("Split Asset"), function() { + frm.trigger("split_asset"); + }, __("Manage")); + if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Adjust Asset Value"), function() { frm.trigger("create_asset_value_adjustment"); @@ -322,6 +326,43 @@ frappe.ui.form.on('Asset', { }); }, + split_asset: function(frm) { + const title = __('Split Asset'); + + const fields = [ + { + fieldname: 'split_qty', + fieldtype: 'Int', + label: __('Split Qty'), + reqd: 1 + } + ]; + + let dialog = new frappe.ui.Dialog({ + title: title, + fields: fields + }); + + dialog.set_primary_action(__('Split'), function() { + const dialog_data = dialog.get_values(); + frappe.call({ + args: { + "asset_name": frm.doc.name, + "split_qty": cint(dialog_data.split_qty) + }, + method: "erpnext.assets.doctype.asset.asset.split_asset", + callback: function(r) { + let doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + + dialog.hide(); + }); + + dialog.show(); + }, + create_asset_value_adjustment: function(frm) { frappe.call({ args: { diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index de060757e2e..6e6bbf1cd29 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -3,7 +3,7 @@ "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", - "creation": "2016-03-01 17:01:27.920130", + "creation": "2022-01-18 02:26:55.975005", "doctype": "DocType", "document_type": "Document", "engine": "InnoDB", @@ -23,6 +23,7 @@ "asset_name", "asset_category", "location", + "split_from", "custodian", "department", "disposal_date", @@ -35,6 +36,7 @@ "available_for_use_date", "column_break_23", "gross_purchase_amount", + "asset_quantity", "purchase_date", "section_break_23", "calculate_depreciation", @@ -141,6 +143,7 @@ }, { "allow_on_submit": 1, + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach Image", "hidden": 1, @@ -480,6 +483,19 @@ "fieldname": "section_break_36", "fieldtype": "Section Break", "label": "Finance Books" + }, + { + "fieldname": "split_from", + "fieldtype": "Link", + "label": "Split From", + "options": "Asset", + "read_only": 1 + }, + { + "fieldname": "asset_quantity", + "fieldtype": "Int", + "label": "Asset Quantity", + "read_only_depends_on": "eval:!doc.is_existing_asset" } ], "idx": 72, @@ -502,10 +518,11 @@ "link_fieldname": "asset" } ], - "modified": "2021-06-24 14:58:51.097908", + "modified": "2022-01-30 20:19:24.680027", "modified_by": "Administrator", "module": "Assets", "name": "Asset", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -542,6 +559,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "asset_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a18b03a8884..6e87426ccbe 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -36,8 +36,10 @@ class Asset(AccountsController): self.validate_asset_values() self.validate_asset_and_reference() self.validate_item() + self.validate_cost_center() self.set_missing_values() - self.prepare_depreciation_data() + if not self.split_from: + self.prepare_depreciation_data() self.validate_gross_and_purchase_amount() if self.get("schedules"): self.validate_expected_value_after_useful_life() @@ -95,6 +97,19 @@ class Asset(AccountsController): elif item.is_stock_item: frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code)) + def validate_cost_center(self): + if not self.cost_center: return + + cost_center_company = frappe.db.get_value('Cost Center', self.cost_center, 'company') + if cost_center_company != self.company: + frappe.throw( + _("Selected Cost Center {} doesn't belongs to {}").format( + frappe.bold(self.cost_center), + frappe.bold(self.company) + ), + title=_("Invalid Cost Center") + ) + def validate_in_use_date(self): if not self.available_for_use_date: frappe.throw(_("Available for use date is required")) @@ -188,142 +203,143 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() for finance_book in self.get('finance_books'): - self.validate_asset_finance_books(finance_book) + self._make_depreciation_schedule(finance_book, start, date_of_sale) - # value_after_depreciation - current Asset value - if self.docstatus == 1 and finance_book.value_after_depreciation: - value_after_depreciation = flt(finance_book.value_after_depreciation) - else: - value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) + def _make_depreciation_schedule(self, finance_book, start, date_of_sale): + self.validate_asset_finance_books(finance_book) - finance_book.value_after_depreciation = value_after_depreciation + value_after_depreciation = self._get_value_after_depreciation(finance_book) + finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ - cint(self.number_of_depreciations_booked) + number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ + cint(self.number_of_depreciations_booked) - has_pro_rata = self.check_is_pro_rata(finance_book) + has_pro_rata = self.check_is_pro_rata(finance_book) + if has_pro_rata: + number_of_pending_depreciations += 1 - if has_pro_rata: - number_of_pending_depreciations += 1 + skip_row = False - skip_row = False + for n in range(start[finance_book.idx-1], number_of_pending_depreciations): + # If depreciation is already completed (for double declining balance) + if skip_row: continue - for n in range(start[finance_book.idx-1], number_of_pending_depreciations): - # If depreciation is already completed (for double declining balance) - if skip_row: continue + depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) - depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) + if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: + schedule_date = add_months(finance_book.depreciation_start_date, + n * cint(finance_book.frequency_of_depreciation)) - if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months(finance_book.depreciation_start_date, - n * cint(finance_book.frequency_of_depreciation)) + # schedule date will be a year later from start date + # so monthly schedule date is calculated by removing 11 months from it + monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) - # schedule date will be a year later from start date - # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) - - # if asset is being sold - if date_of_sale: - from_date = self.get_from_date(finance_book.finance_book) - depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, - from_date, date_of_sale) - - if depreciation_amount > 0: - self.append("schedules", { - "schedule_date": date_of_sale, - "depreciation_amount": depreciation_amount, - "depreciation_method": finance_book.depreciation_method, - "finance_book": finance_book.finance_book, - "finance_book_id": finance_book.idx - }) - - break - - # For first row - if has_pro_rata and not self.opening_accumulated_depreciation and n==0: - depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, - self.available_for_use_date, finance_book.depreciation_start_date) - - # For first depr schedule date will be the start date - # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) - - # For last row - elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: - if not self.flags.increase_in_asset_life: - # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission - self.to_date = add_months(self.available_for_use_date, - (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation)) - - depreciation_amount_without_pro_rata = depreciation_amount - - depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, - depreciation_amount, schedule_date, self.to_date) - - depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, - depreciation_amount, finance_book.finance_book) - - monthly_schedule_date = add_months(schedule_date, 1) - schedule_date = add_days(schedule_date, days) - last_schedule_date = schedule_date - - if not depreciation_amount: continue - value_after_depreciation -= flt(depreciation_amount, - self.precision("gross_purchase_amount")) - - # Adjust depreciation amount in the last period based on the expected value after useful life - if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != finance_book.expected_value_after_useful_life) - or value_after_depreciation < finance_book.expected_value_after_useful_life): - depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) - skip_row = True + # if asset is being sold + if date_of_sale: + from_date = self.get_from_date(finance_book.finance_book) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + from_date, date_of_sale) if depreciation_amount > 0: - # With monthly depreciation, each depreciation is divided by months remaining until next date - if self.allow_monthly_depreciation: - # month range is 1 to 12 - # In pro rata case, for first and last depreciation, month range would be different - month_range = months \ - if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ - else finance_book.frequency_of_depreciation + self._add_depreciation_row(date_of_sale, depreciation_amount, finance_book.depreciation_method, + finance_book.finance_book, finance_book.idx) - for r in range(month_range): - if (has_pro_rata and n == 0): - # For first entry of monthly depr - if r == 0: - days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) - per_day_amt = depreciation_amount / days - depreciation_amount_for_current_month = per_day_amt * days_until_first_depr - depreciation_amount -= depreciation_amount_for_current_month - date = monthly_schedule_date - amount = depreciation_amount_for_current_month - else: - date = add_months(monthly_schedule_date, r) - amount = depreciation_amount / (month_range - 1) - elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1: - # For last entry of monthly depr - date = last_schedule_date - amount = depreciation_amount / month_range + break + + # For first row + if has_pro_rata and not self.opening_accumulated_depreciation and n==0: + from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + from_date, finance_book.depreciation_start_date) + + # For first depr schedule date will be the start date + # so monthly schedule date is calculated by removing month difference between use date and start date + monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) + + # For last row + elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: + if not self.flags.increase_in_asset_life: + # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission + self.to_date = add_months(self.available_for_use_date, + (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation)) + + depreciation_amount_without_pro_rata = depreciation_amount + + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, + depreciation_amount, schedule_date, self.to_date) + + depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, + depreciation_amount, finance_book.finance_book) + + monthly_schedule_date = add_months(schedule_date, 1) + schedule_date = add_days(schedule_date, days) + last_schedule_date = schedule_date + + if not depreciation_amount: continue + value_after_depreciation -= flt(depreciation_amount, + self.precision("gross_purchase_amount")) + + # Adjust depreciation amount in the last period based on the expected value after useful life + if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life) + or value_after_depreciation < finance_book.expected_value_after_useful_life): + depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) + skip_row = True + + if depreciation_amount > 0: + # With monthly depreciation, each depreciation is divided by months remaining until next date + if self.allow_monthly_depreciation: + # month range is 1 to 12 + # In pro rata case, for first and last depreciation, month range would be different + month_range = months \ + if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ + else finance_book.frequency_of_depreciation + + for r in range(month_range): + if (has_pro_rata and n == 0): + # For first entry of monthly depr + if r == 0: + days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + per_day_amt = depreciation_amount / days + depreciation_amount_for_current_month = per_day_amt * days_until_first_depr + depreciation_amount -= depreciation_amount_for_current_month + date = monthly_schedule_date + amount = depreciation_amount_for_current_month else: date = add_months(monthly_schedule_date, r) - amount = depreciation_amount / month_range + amount = depreciation_amount / (month_range - 1) + elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1: + # For last entry of monthly depr + date = last_schedule_date + amount = depreciation_amount / month_range + else: + date = add_months(monthly_schedule_date, r) + amount = depreciation_amount / month_range - self.append("schedules", { - "schedule_date": date, - "depreciation_amount": amount, - "depreciation_method": finance_book.depreciation_method, - "finance_book": finance_book.finance_book, - "finance_book_id": finance_book.idx - }) - else: - self.append("schedules", { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "depreciation_method": finance_book.depreciation_method, - "finance_book": finance_book.finance_book, - "finance_book_id": finance_book.idx - }) + self._add_depreciation_row(date, amount, finance_book.depreciation_method, + finance_book.finance_book, finance_book.idx) + else: + self._add_depreciation_row(schedule_date, depreciation_amount, finance_book.depreciation_method, + finance_book.finance_book, finance_book.idx) + + def _add_depreciation_row(self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id): + self.append("schedules", { + "schedule_date": schedule_date, + "depreciation_amount": depreciation_amount, + "depreciation_method": depreciation_method, + "finance_book": finance_book, + "finance_book_id": finance_book_id + }) + + def _get_value_after_depreciation(self, finance_book): + # value_after_depreciation - current Asset value + if self.docstatus == 1 and finance_book.value_after_depreciation: + value_after_depreciation = flt(finance_book.value_after_depreciation) + else: + value_after_depreciation = (flt(self.gross_purchase_amount) - + flt(self.opening_accumulated_depreciation)) + + return value_after_depreciation # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales # JE: Journal Entry, FB: Finance Book @@ -333,7 +349,6 @@ class Asset(AccountsController): depr_schedule = [] for schedule in self.get('schedules'): - # to update start when there are JEs linked with all the schedule rows corresponding to an FB if len(start) == (int(schedule.finance_book_id) - 2): start.append(num_of_depreciations_completed) @@ -374,7 +389,9 @@ class Asset(AccountsController): if from_date: return from_date - return self.available_for_use_date + + # since depr for available_for_use_date is not yet booked + return add_days(self.available_for_use_date, -1) # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): @@ -608,7 +625,17 @@ class Asset(AccountsController): return purchase_document def get_fixed_asset_account(self): - return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company) + fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company) + if not fixed_asset_account: + frappe.throw( + _("Set {0} in asset category {1} for company {2}").format( + frappe.bold("Fixed Asset Account"), + frappe.bold(self.asset_category), + frappe.bold(self.company), + ), + title=_("Account not Found"), + ) + return fixed_asset_account def get_cwip_account(self, cwip_enabled=False): cwip_account = None @@ -897,3 +924,113 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) return depreciation_amount + +@frappe.whitelist() +def split_asset(asset_name, split_qty): + asset = frappe.get_doc("Asset", asset_name) + split_qty = cint(split_qty) + + if split_qty >= asset.asset_quantity: + frappe.throw(_("Split qty cannot be grater than or equal to asset qty")) + + remaining_qty = asset.asset_quantity - split_qty + + new_asset = create_new_asset_after_split(asset, split_qty) + update_existing_asset(asset, remaining_qty) + + return new_asset + +def update_existing_asset(asset, remaining_qty): + remaining_gross_purchase_amount = flt((asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity) + opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity) + + frappe.db.set_value("Asset", asset.name, { + 'opening_accumulated_depreciation': opening_accumulated_depreciation, + 'gross_purchase_amount': remaining_gross_purchase_amount, + 'asset_quantity': remaining_qty + }) + + for finance_book in asset.get('finance_books'): + value_after_depreciation = flt((finance_book.value_after_depreciation * remaining_qty)/asset.asset_quantity) + expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * remaining_qty)/asset.asset_quantity) + frappe.db.set_value('Asset Finance Book', finance_book.name, 'value_after_depreciation', value_after_depreciation) + frappe.db.set_value('Asset Finance Book', finance_book.name, 'expected_value_after_useful_life', expected_value_after_useful_life) + + accumulated_depreciation = 0 + + for term in asset.get('schedules'): + depreciation_amount = flt((term.depreciation_amount * remaining_qty)/asset.asset_quantity) + frappe.db.set_value('Depreciation Schedule', term.name, 'depreciation_amount', depreciation_amount) + accumulated_depreciation += depreciation_amount + frappe.db.set_value('Depreciation Schedule', term.name, 'accumulated_depreciation_amount', accumulated_depreciation) + +def create_new_asset_after_split(asset, split_qty): + new_asset = frappe.copy_doc(asset) + new_gross_purchase_amount = flt((asset.gross_purchase_amount * split_qty) / asset.asset_quantity) + opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * split_qty) / asset.asset_quantity) + + new_asset.gross_purchase_amount = new_gross_purchase_amount + new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation + new_asset.asset_quantity = split_qty + new_asset.split_from = asset.name + accumulated_depreciation = 0 + + for finance_book in new_asset.get('finance_books'): + finance_book.value_after_depreciation = flt((finance_book.value_after_depreciation * split_qty)/asset.asset_quantity) + finance_book.expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * split_qty)/asset.asset_quantity) + + for term in new_asset.get('schedules'): + depreciation_amount = flt((term.depreciation_amount * split_qty)/asset.asset_quantity) + term.depreciation_amount = depreciation_amount + accumulated_depreciation += depreciation_amount + term.accumulated_depreciation_amount = accumulated_depreciation + + new_asset.submit() + new_asset.set_status() + + for term in new_asset.get('schedules'): + # Update references in JV + if term.journal_entry: + add_reference_in_jv_on_split(term.journal_entry, new_asset.name, asset.name, term.depreciation_amount) + + return new_asset + +def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount): + journal_entry = frappe.get_doc('Journal Entry', entry_name) + entries_to_add = [] + idx = len(journal_entry.get('accounts')) + 1 + + for account in journal_entry.get('accounts'): + if account.reference_name == old_asset_name: + entries_to_add.append(frappe.copy_doc(account).as_dict()) + if account.credit: + account.credit = account.credit - depreciation_amount + account.credit_in_account_currency = account.credit_in_account_currency - \ + account.exchange_rate * depreciation_amount + elif account.debit: + account.debit = account.debit - depreciation_amount + account.debit_in_account_currency = account.debit_in_account_currency - \ + account.exchange_rate * depreciation_amount + + for entry in entries_to_add: + entry.reference_name = new_asset_name + if entry.credit: + entry.credit = depreciation_amount + entry.credit_in_account_currency = entry.exchange_rate * depreciation_amount + elif entry.debit: + entry.debit = depreciation_amount + entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount + + entry.idx = idx + idx += 1 + + journal_entry.append('accounts', entry) + + journal_entry.flags.ignore_validate_update_after_submit = True + journal_entry.save() + + # Repost GL Entries + journal_entry.docstatus = 2 + journal_entry.make_gl_entries(1) + journal_entry.docstatus = 1 + journal_entry.make_gl_entries() \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 44c4ce542d0..c08dc21a8fe 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -7,7 +7,7 @@ import frappe from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -from erpnext.assets.doctype.asset.asset import make_sales_invoice +from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset from erpnext.assets.doctype.asset.depreciation import ( post_depreciation_entries, restore_asset, @@ -134,6 +134,29 @@ class TestAsset(AssetSetup): pr.cancel() self.assertEqual(asset.docstatus, 2) + def test_purchase_of_grouped_asset(self): + create_fixed_asset_item("Rack", is_grouped_asset=1) + pr = make_purchase_receipt(item_code="Rack", qty=3, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + self.assertEqual(asset.asset_quantity, 3) + asset.calculate_depreciation = 1 + + month_end_date = get_last_day(nowdate()) + purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) + + asset.available_for_use_date = purchase_date + asset.purchase_date = purchase_date + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date + }) + asset.submit() + def test_is_fixed_asset_set(self): asset = create_asset(is_existing_asset = 1) doc = frappe.new_doc('Purchase Invoice') @@ -207,9 +230,9 @@ class TestAsset(AssetSetup): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 20392.16, 0.0), + ("_Test Accumulated Depreciations - _TC", 20490.2, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0), ("Debtors - _TC", 25000.0, 0.0) ) @@ -222,6 +245,57 @@ class TestAsset(AssetSetup): si.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + def test_asset_splitting(self): + asset = create_asset( + calculate_depreciation = 1, + asset_quantity=10, + available_for_use_date = '2020-01-01', + purchase_date = '2020-01-01', + expected_value_after_useful_life = 0, + total_number_of_depreciations = 6, + number_of_depreciations_booked = 1, + frequency_of_depreciation = 10, + depreciation_start_date = '2021-01-01', + opening_accumulated_depreciation=20000, + gross_purchase_amount=120000, + submit = 1 + ) + + post_depreciation_entries(date="2021-01-01") + + self.assertEqual(asset.asset_quantity, 10) + self.assertEqual(asset.gross_purchase_amount, 120000) + self.assertEqual(asset.opening_accumulated_depreciation, 20000) + + new_asset = split_asset(asset.name, 2) + asset.load_from_db() + + self.assertEqual(new_asset.asset_quantity, 2) + self.assertEqual(new_asset.gross_purchase_amount, 24000) + self.assertEqual(new_asset.opening_accumulated_depreciation, 4000) + self.assertEqual(new_asset.split_from, asset.name) + self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000) + self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000) + + self.assertEqual(asset.asset_quantity, 8) + self.assertEqual(asset.gross_purchase_amount, 96000) + self.assertEqual(asset.opening_accumulated_depreciation, 16000) + self.assertEqual(asset.schedules[0].depreciation_amount, 16000) + self.assertEqual(asset.schedules[1].depreciation_amount, 16000) + + journal_entry = asset.schedules[0].journal_entry + + jv = frappe.get_doc('Journal Entry', journal_entry) + self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000) + self.assertEqual(jv.accounts[1].debit_in_account_currency, 16000) + self.assertEqual(jv.accounts[2].credit_in_account_currency, 4000) + self.assertEqual(jv.accounts[3].debit_in_account_currency, 4000) + + self.assertEqual(jv.accounts[0].reference_name, asset.name) + self.assertEqual(jv.accounts[1].reference_name, asset.name) + self.assertEqual(jv.accounts[2].reference_name, new_asset.name) + self.assertEqual(jv.accounts[3].reference_name, new_asset.name) + def test_expense_head(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location") @@ -491,10 +565,10 @@ class TestDepreciationMethods(AssetSetup): ) expected_schedules = [ - ["2030-12-31", 27534.25, 27534.25], - ["2031-12-31", 30000.0, 57534.25], - ["2032-12-31", 30000.0, 87534.25], - ["2033-01-30", 2465.75, 90000.0] + ['2030-12-31', 27616.44, 27616.44], + ['2031-12-31', 30000.0, 57616.44], + ['2032-12-31', 30000.0, 87616.44], + ['2033-01-30', 2383.56, 90000.0] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] @@ -544,10 +618,10 @@ class TestDepreciationMethods(AssetSetup): self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) expected_schedules = [ - ["2030-12-31", 28493.15, 28493.15], - ["2031-12-31", 35753.43, 64246.58], - ["2032-12-31", 17876.71, 82123.29], - ["2033-06-06", 5376.71, 87500.0] + ['2030-12-31', 28630.14, 28630.14], + ['2031-12-31', 35684.93, 64315.07], + ['2032-12-31', 17842.47, 82157.54], + ['2033-06-06', 5342.46, 87500.0] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] @@ -580,10 +654,10 @@ class TestDepreciationMethods(AssetSetup): self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) expected_schedules = [ - ["2030-12-31", 11780.82, 11780.82], - ["2031-12-31", 44109.59, 55890.41], - ["2032-12-31", 22054.8, 77945.21], - ["2033-07-12", 9554.79, 87500.0] + ["2030-12-31", 11849.32, 11849.32], + ["2031-12-31", 44075.34, 55924.66], + ["2032-12-31", 22037.67, 77962.33], + ["2033-07-12", 9537.67, 87500.0] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] @@ -621,7 +695,7 @@ class TestDepreciationBasics(AssetSetup): asset = create_asset( item_code = "Macbook Pro", calculate_depreciation = 1, - available_for_use_date = getdate("2019-12-31"), + available_for_use_date = getdate("2020-01-01"), total_number_of_depreciations = 3, expected_value_after_useful_life = 10000, depreciation_start_date = getdate("2020-07-01"), @@ -632,7 +706,7 @@ class TestDepreciationBasics(AssetSetup): ["2020-07-01", 15000, 15000], ["2021-07-01", 30000, 45000], ["2022-07-01", 30000, 75000], - ["2022-12-31", 15000, 90000] + ["2023-01-01", 15000, 90000] ] for i, schedule in enumerate(asset.schedules): @@ -1109,6 +1183,7 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(gle, expected_gle) self.assertEqual(asset.get("value_after_depreciation"), 0) + def test_expected_value_change(self): """ tests if changing `expected_value_after_useful_life` @@ -1130,6 +1205,15 @@ class TestDepreciationBasics(AssetSetup): asset.reload() self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) + def test_asset_cost_center(self): + asset = create_asset(is_existing_asset = 1, do_not_save=1) + asset.cost_center = "Main - WP" + + self.assertRaises(frappe.ValidationError, asset.submit) + + asset.cost_center = "Main - _TC" + asset.submit() + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): create_asset_category() @@ -1164,7 +1248,8 @@ def create_asset(**args): "available_for_use_date": args.available_for_use_date or "2020-06-06", "location": args.location or "Test Location", "asset_owner": args.asset_owner or "Company", - "is_existing_asset": args.is_existing_asset or 1 + "is_existing_asset": args.is_existing_asset or 1, + "asset_quantity": args.get("asset_quantity") or 1 }) if asset.calculate_depreciation: @@ -1202,13 +1287,13 @@ def create_asset_category(): }) asset_category.insert() -def create_fixed_asset_item(): +def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_asset=0): meta = frappe.get_meta('Asset') naming_series = meta.get_field("naming_series").options.splitlines()[0] or 'ACC-ASS-.YYYY.-' try: - frappe.get_doc({ + item = frappe.get_doc({ "doctype": "Item", - "item_code": "Macbook Pro", + "item_code": item_code or "Macbook Pro", "item_name": "Macbook Pro", "description": "Macbook Pro Retina Display", "asset_category": "Computers", @@ -1216,11 +1301,14 @@ def create_fixed_asset_item(): "stock_uom": "Nos", "is_stock_item": 0, "is_fixed_asset": 1, - "auto_create_assets": 1, + "auto_create_assets": auto_create_assets, + "is_grouped_asset": is_grouped_asset, "asset_naming_series": naming_series - }).insert() + }) + item.insert() except frappe.DuplicateEntryError: pass + return item def set_depreciation_settings_in_company(): company = frappe.get_doc("Company", "_Test Company") diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json index 495de46e414..26a6609b310 100644 --- a/erpnext/assets/workspace/assets/assets.json +++ b/erpnext/assets/workspace/assets/assets.json @@ -5,7 +5,7 @@ "label": "Asset Value Analytics" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Assets\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Asset Value Analytics\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Asset\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Asset Category\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Fixed Asset Register\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assets\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Maintenance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Assets\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Asset Value Analytics\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset Category\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Fixed Asset Register\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assets\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "creation": "2020-03-02 15:43:27.634865", "docstatus": 0, "doctype": "Workspace", @@ -172,7 +172,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:15:54.839453", + "modified": "2022-01-13 17:25:41.730628", "modified_by": "Administrator", "module": "Assets", "name": "Assets", @@ -181,7 +181,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 4, + "sequence_id": 4.0, "shortcuts": [ { "label": "Asset", diff --git a/erpnext/buying/workspace/buying/buying.json b/erpnext/buying/workspace/buying/buying.json index 380ef3639f6..5ad93f0e594 100644 --- a/erpnext/buying/workspace/buying/buying.json +++ b/erpnext/buying/workspace/buying/buying.json @@ -5,7 +5,7 @@ "label": "Purchase Order Trends" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Buying\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Purchase Order Trends\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Material Request\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Analytics\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Order Analysis\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Buying\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Items & Pricing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Supplier\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Supplier Scorecard\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Regional\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Buying\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Purchase Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Buying\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items & Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Supplier\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Supplier Scorecard\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Regional\",\"col\":4}}]", "creation": "2020-01-28 11:50:26.195467", "docstatus": 0, "doctype": "Workspace", @@ -509,7 +509,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:15:56.218428", + "modified": "2022-01-13 17:26:39.090190", "modified_by": "Administrator", "module": "Buying", "name": "Buying", @@ -518,7 +518,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 6, + "sequence_id": 6.0, "shortcuts": [ { "color": "Green", diff --git a/erpnext/commands/__init__.py b/erpnext/commands/__init__.py index 59311192148..8e12fad3d75 100644 --- a/erpnext/commands/__init__.py +++ b/erpnext/commands/__init__.py @@ -1,49 +1,10 @@ -# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# GPL v3 License. See license.txt import click -import frappe -from frappe.commands import get_site, pass_context def call_command(cmd, context): return click.Context(cmd, obj=context).forward(cmd) -@click.command('make-demo') -@click.option('--site', help='site name') -@click.option('--domain', default='Manufacturing') -@click.option('--days', default=100, - help='Run the demo for so many days. Default 100') -@click.option('--resume', default=False, is_flag=True, - help='Continue running the demo for given days') -@click.option('--reinstall', default=False, is_flag=True, - help='Reinstall site before demo') -@pass_context -def make_demo(context, site, domain='Manufacturing', days=100, - resume=False, reinstall=False): - "Reinstall site and setup demo" - from frappe.commands.site import _reinstall - from frappe.installer import install_app - - site = get_site(context) - - if resume: - with frappe.init_site(site): - frappe.connect() - from erpnext.demo import demo - demo.simulate(days=days) - else: - if reinstall: - _reinstall(site, yes=True) - with frappe.init_site(site=site): - frappe.connect() - if not 'erpnext' in frappe.get_installed_apps(): - install_app('erpnext') - - # import needs site - from erpnext.demo import demo - demo.make(domain, days) - -commands = [ - make_demo -] +commands = [] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index eab9e126412..bab16a494d6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -167,9 +167,14 @@ class AccountsController(TransactionBase): validate_regional(self) + validate_einvoice_fields(self) + if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) + def before_cancel(self): + validate_einvoice_fields(self) + def on_trash(self): # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'): @@ -185,8 +190,6 @@ class AccountsController(TransactionBase): frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) elif getdate(self.posting_date) > getdate(d.service_end_date): frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)) - elif getdate(self.posting_date) > getdate(d.service_start_date): - frappe.throw(_("Row #{0}: Service Start Date cannot be before Invoice Posting Date").format(d.idx)) def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() @@ -2153,3 +2156,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil @erpnext.allow_regional def validate_regional(doc): pass + +@erpnext.allow_regional +def validate_einvoice_fields(doc): + pass diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index a3d2502268e..a181af73133 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -70,9 +70,18 @@ class BuyingController(StockController, Subcontracting): # set contact and address details for supplier, if they are not mentioned if getattr(self, "supplier", None): - self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions, - doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'), - fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'))) + self.update_if_missing( + get_party_details( + self.supplier, + party_type="Supplier", + doctype=self.doctype, + company=self.company, + party_address=self.get("supplier_address"), + shipping_address=self.get('shipping_address'), + fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'), + ignore_permissions=self.flags.ignore_permissions + ) + ) self.set_missing_item_details(for_validate) @@ -554,10 +563,13 @@ class BuyingController(StockController, Subcontracting): # Check for asset naming series if item_data.get('asset_naming_series'): created_assets = [] - - for qty in range(cint(d.qty)): - asset = self.make_asset(d) + if item_data.get('is_grouped_asset'): + asset = self.make_asset(d, is_grouped_asset=True) created_assets.append(asset) + else: + for qty in range(cint(d.qty)): + asset = self.make_asset(d) + created_assets.append(asset) if len(created_assets) > 5: # dont show asset form links if more than 5 assets are created @@ -580,14 +592,18 @@ class BuyingController(StockController, Subcontracting): for message in messages: frappe.msgprint(message, title="Success", indicator="green") - def make_asset(self, row): + def make_asset(self, row, is_grouped_asset=False): if not row.asset_location: frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code)) item_data = frappe.db.get_value('Item', row.item_code, ['asset_naming_series', 'asset_category'], as_dict=1) - purchase_amount = flt(row.base_rate + row.item_tax_amount) + if is_grouped_asset: + purchase_amount = flt(row.base_amount + row.item_tax_amount) + else: + purchase_amount = flt(row.base_rate + row.item_tax_amount) + asset = frappe.get_doc({ 'doctype': 'Asset', 'item_code': row.item_code, @@ -601,6 +617,7 @@ class BuyingController(StockController, Subcontracting): 'calculate_depreciation': 1, 'purchase_receipt_amount': purchase_amount, 'gross_purchase_amount': purchase_amount, + 'asset_quantity': row.qty if is_grouped_asset else 0, 'purchase_receipt': self.name if self.doctype == 'Purchase Receipt' else None, 'purchase_invoice': self.name if self.doctype == 'Purchase Invoice' else None }) @@ -687,7 +704,7 @@ class BuyingController(StockController, Subcontracting): 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"], + for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series", "is_grouped_asset"], filters = {'name': ('in', asset_items)}): asset_items_data.setdefault(d.name, d) diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py index b8dc92efdeb..ae2c73758cb 100644 --- a/erpnext/controllers/employee_boarding_controller.py +++ b/erpnext/controllers/employee_boarding_controller.py @@ -132,13 +132,17 @@ class EmployeeBoardingController(Document): def on_cancel(self): # delete task project - for task in frappe.get_all('Task', filters={'project': self.project}): + project = self.project + for task in frappe.get_all('Task', filters={'project': project}): frappe.delete_doc('Task', task.name, force=1) - frappe.delete_doc('Project', self.project, force=1) + frappe.delete_doc('Project', project, force=1) self.db_set('project', '') for activity in self.activities: activity.db_set('task', '') + frappe.msgprint(_('Linked Project {} and Tasks deleted.').format( + project), alert=True, indicator='blue') + @frappe.whitelist() def get_onboarding_details(parent, parenttype): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index cc773b75963..4ff851d7f94 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -385,7 +385,7 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get('stock_qty') or d.get('actual_qty')) - if not d.incoming_rate: + if not (self.get("is_return") and d.incoming_rate): d.incoming_rate = get_incoming_rate({ "item_code": d.item_code, "warehouse": d.warehouse, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 7073e32f536..2912d3eb0bd 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -17,7 +17,7 @@ from erpnext.accounts.general_ledger import ( 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_items_to_be_repost, get_valuation_rate +from erpnext.stock.stock_ledger import get_items_to_be_repost class QualityInspectionRequiredError(frappe.ValidationError): pass @@ -77,17 +77,17 @@ class StockController(AccountsController): .format(d.idx, get_link_to_form("Batch", d.get("batch_no")))) def clean_serial_nos(self): + from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string + for row in self.get("items"): if hasattr(row, "serial_no") and row.serial_no: - # replace commas by linefeed - row.serial_no = row.serial_no.replace(",", "\n") + # remove extra whitespace and store one serial no on each line + row.serial_no = clean_serial_no_string(row.serial_no) - # strip preceeding and succeeding spaces for each SN - # (SN could have valid spaces in between e.g. SN - 123 - 2021) - serial_no_list = row.serial_no.split("\n") - serial_no_list = [sn.strip() for sn in serial_no_list] - - row.serial_no = "\n".join(serial_no_list) + for row in self.get('packed_items') or []: + if hasattr(row, "serial_no") and row.serial_no: + # remove extra whitespace and store one serial no on each line + row.serial_no = clean_serial_no_string(row.serial_no) def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): @@ -111,17 +111,6 @@ class StockController(AccountsController): self.check_expense_account(item_row) - # If the item does not have the allow zero valuation rate flag set - # and ( valuation rate not mentioned in an incoming entry - # or incoming entry not found while delivering the item), - # try to pick valuation rate from previous sle or Item master and update in SLE - # Otherwise, throw an exception - - if not sle.stock_value_difference and self.doctype != "Stock Reconciliation" \ - and not item_row.get("allow_zero_valuation_rate"): - - sle = self.update_stock_ledger_entries(sle) - # expense account/ target_warehouse / source_warehouse if item_row.get('target_warehouse'): warehouse = item_row.get('target_warehouse') @@ -164,26 +153,6 @@ class StockController(AccountsController): return frappe.flags.debit_field_precision - def update_stock_ledger_entries(self, sle): - sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, - self.doctype, self.name, currency=self.company_currency, company=self.company) - - sle.stock_value = flt(sle.qty_after_transaction) * flt(sle.valuation_rate) - sle.stock_value_difference = flt(sle.actual_qty) * flt(sle.valuation_rate) - - if sle.name: - frappe.db.sql(""" - update - `tabStock Ledger Entry` - set - stock_value = %(stock_value)s, - valuation_rate = %(valuation_rate)s, - stock_value_difference = %(stock_value_difference)s - where - name = %(name)s""", (sle)) - - return sle - def get_voucher_details(self, default_expense_account, default_cost_center, sle_map): if self.doctype == "Stock Reconciliation": reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose") @@ -287,11 +256,7 @@ class StockController(AccountsController): for d in self.items: if not d.batch_no: continue - serial_nos = [sr.name for sr in frappe.get_all("Serial No", - {'batch_no': d.batch_no, 'status': 'Inactive'})] - - if serial_nos: - frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None) + frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None) d.batch_no = None d.db_set("batch_no", None) diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py index 13aa697610e..f4d3f97ef0d 100644 --- a/erpnext/controllers/tests/test_transaction_base.py +++ b/erpnext/controllers/tests/test_transaction_base.py @@ -4,19 +4,72 @@ import frappe class TestUtils(unittest.TestCase): - def test_reset_default_field_value(self): - doc = frappe.get_doc({ - "doctype": "Purchase Receipt", - "set_warehouse": "Warehouse 1", - }) + def test_reset_default_field_value(self): + doc = frappe.get_doc({ + "doctype": "Purchase Receipt", + "set_warehouse": "Warehouse 1", + }) - # Same values - doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] - doc.reset_default_field_value("set_warehouse", "items", "warehouse") - self.assertEqual(doc.set_warehouse, "Warehouse 1") + # Same values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, "Warehouse 1") - # Mixed values - doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] - doc.reset_default_field_value("set_warehouse", "items", "warehouse") - self.assertEqual(doc.set_warehouse, None) + # Mixed values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, None) + def test_reset_default_field_value_in_mfg_stock_entry(self): + # manufacture stock entry with rows having blank source/target wh + se = frappe.get_doc( + doctype="Stock Entry", + purpose="Manufacture", + stock_entry_type="Manufacture", + company="_Test Company", + from_warehouse="_Test Warehouse - _TC", + to_warehouse="_Test Warehouse 1 - _TC", + items=[ + frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"), + frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1) + ] + ) + se.save() + + # default fields must be untouched + self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC") + self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC") + + se.delete() + + def test_reset_default_field_value_in_transfer_stock_entry(self): + doc = frappe.get_doc({ + "doctype": "Stock Entry", + "purpose": "Material Receipt", + "from_warehouse": "Warehouse 1", + "to_warehouse": "Warehouse 2", + }) + + # Same values + doc.items = [ + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"} + ] + + doc.reset_default_field_value("from_warehouse", "items", "s_warehouse") + doc.reset_default_field_value("to_warehouse", "items", "t_warehouse") + self.assertEqual(doc.from_warehouse, "Warehouse 1") + self.assertEqual(doc.to_warehouse, "Warehouse 2") + + # Mixed values in source wh + doc.items = [ + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"} + ] + + doc.reset_default_field_value("from_warehouse", "items", "s_warehouse") + doc.reset_default_field_value("to_warehouse", "items", "t_warehouse") + self.assertEqual(doc.from_warehouse, None) + self.assertEqual(doc.to_warehouse, "Warehouse 2") \ No newline at end of file diff --git a/erpnext/crm/doctype/campaign/campaign.js b/erpnext/crm/doctype/campaign/campaign.js index 11bfa74b29c..cac45c682cb 100644 --- a/erpnext/crm/doctype/campaign/campaign.js +++ b/erpnext/crm/doctype/campaign/campaign.js @@ -5,7 +5,7 @@ frappe.ui.form.on('Campaign', { refresh: function(frm) { erpnext.toggle_naming_series(); - if (frm.doc.__islocal) { + if (frm.is_new()) { frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series"); } else { cur_frm.add_custom_button(__("View Leads"), function() { diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py index bde52547c95..98cf7d845e0 100644 --- a/erpnext/crm/doctype/crm_settings/crm_settings.py +++ b/erpnext/crm/doctype/crm_settings/crm_settings.py @@ -1,9 +1,10 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document class CRMSettings(Document): - pass + def validate(self): + frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", "")) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index 8fd4978715f..d2ac10adea2 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -2,13 +2,14 @@ # For license information, please see license.txt +from urllib.parse import urlencode + import frappe import requests from frappe import _ from frappe.model.document import Document from frappe.utils import get_url_to_form from frappe.utils.file_manager import get_file_path -from six.moves.urllib.parse import urlencode class LinkedInSettings(Document): diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index f8376e6ca94..8e7d67e0575 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -24,6 +24,14 @@ frappe.ui.form.on("Opportunity", { frm.trigger('set_contact_link'); } }, + + validate: function(frm) { + if (frm.doc.status == "Lost" && !frm.doc.lost_reasons.length) { + frm.trigger('set_as_lost_dialog'); + frappe.throw(__("Lost Reasons are required in case opportunity is Lost.")); + } + }, + contact_date: function(frm) { if(frm.doc.contact_date < frappe.datetime.now_datetime()){ frm.set_value("contact_date", ""); @@ -82,7 +90,7 @@ frappe.ui.form.on("Opportunity", { frm.trigger('setup_opportunity_from'); erpnext.toggle_naming_series(); - if(!doc.__islocal && doc.status!=="Lost") { + if(!frm.is_new() && doc.status!=="Lost") { if(doc.with_items){ frm.add_custom_button(__('Supplier Quotation'), function() { @@ -187,11 +195,11 @@ frappe.ui.form.on("Opportunity", { change_form_labels: function(frm) { let company_currency = erpnext.get_currency(frm.doc.company); - frm.set_currency_labels(["base_opportunity_amount", "base_total", "base_grand_total"], company_currency); - frm.set_currency_labels(["opportunity_amount", "total", "grand_total"], frm.doc.currency); + frm.set_currency_labels(["base_opportunity_amount", "base_total"], company_currency); + frm.set_currency_labels(["opportunity_amount", "total"], frm.doc.currency); // toggle fields - frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total", "base_grand_total"], + frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total"], frm.doc.currency != company_currency); }, @@ -209,20 +217,15 @@ frappe.ui.form.on("Opportunity", { }, calculate_total: function(frm) { - let total = 0, base_total = 0, grand_total = 0, base_grand_total = 0; + let total = 0, base_total = 0; frm.doc.items.forEach(item => { total += item.amount; base_total += item.base_amount; }) - base_grand_total = base_total + frm.doc.base_opportunity_amount; - grand_total = total + frm.doc.opportunity_amount; - frm.set_value({ 'total': flt(total), - 'base_total': flt(base_total), - 'grand_total': flt(grand_total), - 'base_grand_total': flt(base_grand_total) + 'base_total': flt(base_total) }); } diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index dc32d9a4124..089f2d2faa8 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -42,10 +42,8 @@ "items", "section_break_32", "base_total", - "base_grand_total", "column_break_33", "total", - "grand_total", "contact_info", "customer_address", "address_display", @@ -475,21 +473,6 @@ "fieldname": "column_break_33", "fieldtype": "Column Break" }, - { - "fieldname": "base_grand_total", - "fieldtype": "Currency", - "label": "Grand Total (Company Currency)", - "options": "Company:company:default_currency", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "grand_total", - "fieldtype": "Currency", - "label": "Grand Total", - "options": "currency", - "read_only": 1 - }, { "fieldname": "lost_detail_section", "fieldtype": "Section Break", @@ -510,7 +493,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2021-10-21 12:04:30.151379", + "modified": "2022-01-29 19:32:26.382896", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", @@ -547,6 +530,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "subject_field": "title", "timeline_field": "party_name", "title_field": "title", diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index a4fd7658eed..2d538748ec2 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -69,8 +69,6 @@ class Opportunity(TransactionBase): self.total = flt(total) self.base_total = flt(base_total) - self.grand_total = flt(self.total) + flt(self.opportunity_amount) - self.base_grand_total = flt(self.base_total) + flt(self.base_opportunity_amount) def make_new_lead_if_required(self): """Set lead against new opportunity""" diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js index 67018e1ef9c..8721a5b42d3 100644 --- a/erpnext/crm/doctype/prospect/prospect.js +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -3,6 +3,8 @@ frappe.ui.form.on('Prospect', { refresh (frm) { + frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: frm.doctype }; + if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) { frm.add_custom_button(__("Customer"), function() { frappe.model.open_mapped_doc({ diff --git a/erpnext/crm/module_onboarding/crm/crm.json b/erpnext/crm/module_onboarding/crm/crm.json index 8315218c842..0faad1d3151 100644 --- a/erpnext/crm/module_onboarding/crm/crm.json +++ b/erpnext/crm/module_onboarding/crm/crm.json @@ -16,7 +16,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/CRM", "idx": 0, "is_complete": 0, - "modified": "2020-07-08 14:05:42.644448", + "modified": "2022-01-29 20:14:29.502145", "modified_by": "Administrator", "module": "CRM", "name": "CRM", @@ -33,6 +33,9 @@ }, { "step": "Create and Send Quotation" + }, + { + "step": "CRM Settings" } ], "subtitle": "Lead, Opportunity, Customer, and more.", diff --git a/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json b/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json index 78f7e4de9c7..f0f50de5d16 100644 --- a/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json +++ b/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json @@ -5,7 +5,6 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-05-28 21:07:11.461172", @@ -13,6 +12,7 @@ "name": "Create and Send Quotation", "owner": "Administrator", "reference_document": "Quotation", + "show_form_tour": 0, "show_full_form": 1, "title": "Create and Send Quotation", "validate_action": 1 diff --git a/erpnext/crm/onboarding_step/create_lead/create_lead.json b/erpnext/crm/onboarding_step/create_lead/create_lead.json index c45e8b036c5..cb5cce66a5f 100644 --- a/erpnext/crm/onboarding_step/create_lead/create_lead.json +++ b/erpnext/crm/onboarding_step/create_lead/create_lead.json @@ -5,7 +5,6 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-05-28 21:07:01.373403", @@ -13,6 +12,7 @@ "name": "Create Lead", "owner": "Administrator", "reference_document": "Lead", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Lead", "validate_action": 1 diff --git a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json index 0ee9317c852..96e0256d700 100644 --- a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json +++ b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json @@ -5,7 +5,6 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2021-01-21 15:28:52.483839", @@ -13,6 +12,7 @@ "name": "Create Opportunity", "owner": "Administrator", "reference_document": "Opportunity", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Opportunity", "validate_action": 1 diff --git a/erpnext/crm/onboarding_step/crm_settings/crm_settings.json b/erpnext/crm/onboarding_step/crm_settings/crm_settings.json new file mode 100644 index 00000000000..555d795987f --- /dev/null +++ b/erpnext/crm/onboarding_step/crm_settings/crm_settings.json @@ -0,0 +1,21 @@ +{ + "action": "Go to Page", + "creation": "2022-01-29 20:14:24.803844", + "description": "# CRM Settings\n\nCRM module\u2019s features are configurable as per your business needs. CRM Settings is the place where you can set your preferences for:\n- Campaign\n- Lead\n- Opportunity\n- Quotation", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2022-01-29 20:14:24.803844", + "modified_by": "Administrator", + "name": "CRM Settings", + "owner": "Administrator", + "path": "#crm-settings/CRM%20Settings", + "reference_document": "CRM Settings", + "show_form_tour": 0, + "show_full_form": 0, + "title": "CRM Settings", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json b/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json index fa26921ae2c..88717530945 100644 --- a/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json +++ b/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json @@ -5,13 +5,13 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-05-14 17:28:16.448676", "modified_by": "Administrator", "name": "Introduction to CRM", "owner": "Administrator", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to CRM", "validate_action": 1, diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json index 5a63dc18d05..83341f5a219 100644 --- a/erpnext/crm/workspace/crm/crm.json +++ b/erpnext/crm/workspace/crm/crm.json @@ -1,10 +1,11 @@ { "charts": [ { - "chart_name": "Territory Wise Sales" + "chart_name": "Territory Wise Sales", + "label": "Territory Wise Sales" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"CRM\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Lead\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Opportunity\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customer\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Analytics\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Sales Pipeline\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Maintenance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Campaign\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"CRM\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Territory Wise Sales\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Opportunity\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Sales Pipeline\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Campaign\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-01-23 14:48:30.183272", "docstatus": 0, "doctype": "Workspace", @@ -144,6 +145,7 @@ "hidden": 0, "is_query_report": 1, "label": "Sales Pipeline Analytics", + "link_count": 0, "link_to": "Sales Pipeline Analytics", "link_type": "Report", "onboard": 0, @@ -153,6 +155,7 @@ "hidden": 0, "is_query_report": 1, "label": "Opportunity Summary by Sales Stage", + "link_count": 0, "link_to": "Opportunity Summary by Sales Stage", "link_type": "Report", "onboard": 0, @@ -414,7 +417,7 @@ "type": "Link" } ], - "modified": "2021-08-20 12:15:56.913092", + "modified": "2022-01-13 17:53:17.509844", "modified_by": "Administrator", "module": "CRM", "name": "CRM", @@ -423,7 +426,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 7, + "sequence_id": 7.0, "shortcuts": [ { "color": "Blue", diff --git a/erpnext/demo/data/account.json b/erpnext/demo/data/account.json deleted file mode 100644 index b50b0c94b0f..00000000000 --- a/erpnext/demo/data/account.json +++ /dev/null @@ -1,18 +0,0 @@ -[{ - "account_name": "Debtors EUR", - "parent_account": "Accounts Receivable", - "account_type": "Receivable", - "account_currency": "EUR" -}, -{ - "account_name": "Creditors EUR", - "parent_account": "Accounts Payable", - "account_type": "Payable", - "account_currency": "EUR" -}, -{ - "account_name": "Paypal", - "parent_account": "Bank Accounts", - "account_type": "Bank", - "account_currency": "EUR" -}] \ No newline at end of file diff --git a/erpnext/demo/data/address.json b/erpnext/demo/data/address.json deleted file mode 100644 index 7618c2cf337..00000000000 --- a/erpnext/demo/data/address.json +++ /dev/null @@ -1,218 +0,0 @@ -[ - { - "address_line1": "254 Theotokopoulou Str.", - "address_type": "Office", - "city": "Larnaka", - "country": "Cyprus", - "links": [{"link_doctype": "Customer", "link_name": "Adaptas"}], - "phone": "23566775757" - }, - { - "address_line1": "R Patr\u00e3o Caramelho 116", - "address_type": "Office", - "city": "Fajozes", - "country": "Portugal", - "links": [{"link_doctype": "Customer", "link_name": "Asian Fusion"}], - "phone": "23566775757" - }, - { - "address_line1": "30 Fulford Road", - "address_type": "Office", - "city": "PENTRE-PIOD", - "country": "United Kingdom", - "links": [{"link_doctype": "Customer", "link_name": "Asian Junction"}], - "phone": "23566775757" - }, - { - "address_line1": "Schoenebergerstrasse 13", - "address_type": "Office", - "city": "Raschau", - "country": "Germany", - "links": [{"link_doctype": "Customer", "link_name": "Big D Supermarkets"}], - "phone": "23566775757" - }, - { - "address_line1": "Hoheluftchaussee 43", - "address_type": "Office", - "city": "Kieritzsch", - "country": "Germany", - "links": [{"link_doctype": "Customer", "link_name": "Buttrey Food & Drug"}], - "phone": "23566775757" - }, - { - "address_line1": "R Cimo Vila 6", - "address_type": "Office", - "city": "Rebordosa", - "country": "Portugal", - "links": [{"link_doctype": "Customer", "link_name": "Chi-Chis"}], - "phone": "23566775757" - }, - { - "address_line1": "R 5 Outubro 9", - "address_type": "Office", - "city": "Quinta Nova S\u00e3o Domingos", - "country": "Portugal", - "links": [{"link_doctype": "Customer", "link_name": "Choices"}], - "phone": "23566775757" - }, - { - "address_line1": "Avenida Macambira 953", - "address_type": "Office", - "city": "Goi\u00e2nia", - "country": "Brazil", - "links": [{"link_doctype": "Customer", "link_name": "Consumers and Consumers Express"}], - "phone": "23566775757" - }, - { - "address_line1": "2342 Goyeau Ave", - "address_type": "Office", - "city": "Windsor", - "country": "Canada", - "links": [{"link_doctype": "Customer", "link_name": "Crafts Canada"}], - "phone": "23566775757" - }, - { - "address_line1": "Laukaantie 82", - "address_type": "Office", - "city": "KOKKOLA", - "country": "Finland", - "links": [{"link_doctype": "Customer", "link_name": "Endicott Shoes"}], - "phone": "23566775757" - }, - { - "address_line1": "9 Brown Street", - "address_type": "Office", - "city": "PETERSHAM", - "country": "Australia", - "links": [{"link_doctype": "Customer", "link_name": "Fayva"}], - "phone": "23566775757" - }, - { - "address_line1": "Via Donnalbina 41", - "address_type": "Office", - "city": "Cala Gonone", - "country": "Italy", - "links": [{"link_doctype": "Customer", "link_name": "Intelacard"}], - "phone": "23566775757" - }, - { - "address_line1": "Liljerum Grenadj\u00e4rtorpet 69", - "address_type": "Office", - "city": "TOMTEBODA", - "country": "Sweden", - "links": [{"link_doctype": "Customer", "link_name": "Landskip Yard Care"}], - "phone": "23566775757" - }, - { - "address_line1": "72 Bishopgate Street", - "address_type": "Office", - "city": "SEAHAM", - "country": "United Kingdom", - "links": [{"link_doctype": "Customer", "link_name": "Life Plan Counselling"}], - "phone": "23566775757" - }, - { - "address_line1": "\u03a3\u03ba\u03b1\u03c6\u03af\u03b4\u03b9\u03b1 105", - "address_type": "Office", - "city": "\u03a0\u0391\u03a1\u0395\u039a\u039a\u039b\u0397\u03a3\u0399\u0391", - "country": "Cyprus", - "links": [{"link_doctype": "Customer", "link_name": "Mr Fables"}], - "phone": "23566775757" - }, - { - "address_line1": "Mellemvej 7", - "address_type": "Office", - "city": "Aabybro", - "country": "Denmark", - "links": [{"link_doctype": "Customer", "link_name": "Nelson Brothers"}], - "phone": "23566775757" - }, - { - "address_line1": "Plougg\u00e5rdsvej 98", - "address_type": "Office", - "city": "Karby", - "country": "Denmark", - "links": [{"link_doctype": "Customer", "link_name": "Netobill"}], - "phone": "23566775757" - }, - { - "address_line1": "176 Michalakopoulou Street", - "address_type": "Office", - "city": "Agio Georgoudi", - "country": "Cyprus", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "Helios Air"}] - }, - { - "address_line1": "Fibichova 1102", - "address_type": "Office", - "city": "Kokor\u00edn", - "country": "Czech Republic", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "Ks Merchandise"}] - }, - { - "address_line1": "Zahradn\u00ed 888", - "address_type": "Office", - "city": "Cecht\u00edn", - "country": "Czech Republic", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "HomeBase"}] - }, - { - "address_line1": "ul. Grochowska 94", - "address_type": "Office", - "city": "Warszawa", - "country": "Poland", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "Scott Ties"}] - }, - { - "address_line1": "Norra Esplanaden 87", - "address_type": "Office", - "city": "HELSINKI", - "country": "Finland", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "Reliable Investments"}] - }, - { - "address_line1": "2038 Fallon Drive", - "address_type": "Office", - "city": "Dresden", - "country": "Canada", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "Nan Duskin"}] - }, - { - "address_line1": "77 cours Franklin Roosevelt", - "address_type": "Office", - "city": "MARSEILLE", - "country": "France", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "Rainbow Records"}] - }, - { - "address_line1": "ul. Tuwima Juliana 85", - "address_type": "Office", - "city": "\u0141\u00f3d\u017a", - "country": "Poland", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "New World Realty"}] - }, - { - "address_line1": "Gl. Sygehusvej 41", - "address_type": "Office", - "city": "Narsaq", - "country": "Greenland", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "Asiatic Solutions"}] - }, - { - "address_line1": "Gosposka ulica 50", - "address_type": "Office", - "city": "Nova Gorica", - "country": "Slovenia", - "phone": "23566775757", - "links": [{"link_doctype": "Supplier", "link_name": "Eagle Hardware"}] - } -] \ No newline at end of file diff --git a/erpnext/demo/data/assessment_criteria.json b/erpnext/demo/data/assessment_criteria.json deleted file mode 100644 index 82956822a2a..00000000000 --- a/erpnext/demo/data/assessment_criteria.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "doctype": "Assessment Criteria", - "assessment_criteria": "Aptitude" - }, - { - "doctype": "Assessment Criteria", - "assessment_criteria": "Application" - }, - { - "doctype": "Assessment Criteria", - "assessment_criteria": "Understanding" - }, - { - "doctype": "Assessment Criteria", - "assessment_criteria": "Knowledge" - } -] \ No newline at end of file diff --git a/erpnext/demo/data/asset.json b/erpnext/demo/data/asset.json deleted file mode 100644 index 44db2ae9e1b..00000000000 --- a/erpnext/demo/data/asset.json +++ /dev/null @@ -1,58 +0,0 @@ -[ - { - "asset_name": "Macbook Pro - 1", - "item_code": "Computer", - "gross_purchase_amount": 100000, - "asset_owner": "Company", - "available_for_use_date": "2017-01-02", - "location": "Main Location" - }, - { - "asset_name": "Macbook Air - 1", - "item_code": "Computer", - "gross_purchase_amount": 60000, - "asset_owner": "Company", - "available_for_use_date": "2017-10-02", - "location": "Avg Location" - }, - { - "asset_name": "Conferrence Table", - "item_code": "Table", - "gross_purchase_amount": 30000, - "asset_owner": "Company", - "available_for_use_date": "2018-10-02", - "location": "Zany Location" - }, - { - "asset_name": "Lunch Table", - "item_code": "Table", - "gross_purchase_amount": 20000, - "asset_owner": "Company", - "available_for_use_date": "2018-06-02", - "location": "Fletcher Location" - }, - { - "asset_name": "ERPNext", - "item_code": "ERP", - "gross_purchase_amount": 100000, - "asset_owner": "Company", - "available_for_use_date": "2018-09-02", - "location":"Main Location" - }, - { - "asset_name": "Chair 1", - "item_code": "Chair", - "gross_purchase_amount": 10000, - "asset_owner": "Company", - "available_for_use_date": "2018-07-02", - "location": "Zany Location" - }, - { - "asset_name": "Chair 2", - "item_code": "Chair", - "gross_purchase_amount": 10000, - "asset_owner": "Company", - "available_for_use_date": "2018-07-02", - "location": "Avg Location" - } -] diff --git a/erpnext/demo/data/asset_category.json b/erpnext/demo/data/asset_category.json deleted file mode 100644 index 54f779da96f..00000000000 --- a/erpnext/demo/data/asset_category.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "asset_category_name": "Furnitures", - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 5, - "frequency_of_depreciation": 12, - "accounts": [{ - "company_name": "Wind Power LLC", - "fixed_asset_account": "Furnitures and Fixtures - WPL", - "accumulated_depreciation_account": "Accumulated Depreciation - WPL", - "depreciation_expense_account": "Depreciation - WPL" - }] - }, - { - "asset_category_name": "Electronic Equipments", - "depreciation_method": "Double Declining Balance", - "total_number_of_depreciations": 10, - "frequency_of_depreciation": 6, - "accounts": [{ - "company_name": "Wind Power LLC", - "fixed_asset_account": "Electronic Equipments - WPL", - "accumulated_depreciation_account": "Accumulated Depreciation - WPL", - "depreciation_expense_account": "Depreciation - WPL" - }] - }, - { - "asset_category_name": "Softwares", - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 10, - "frequency_of_depreciation": 12, - "accounts": [{ - "company_name": "Wind Power LLC", - "fixed_asset_account": "Softwares - WPL", - "accumulated_depreciation_account": "Accumulated Depreciation - WPL", - "depreciation_expense_account": "Depreciation - WPL" - }] - } -] \ No newline at end of file diff --git a/erpnext/demo/data/bom.json b/erpnext/demo/data/bom.json deleted file mode 100644 index 30854359b29..00000000000 --- a/erpnext/demo/data/bom.json +++ /dev/null @@ -1,180 +0,0 @@ -[ - { - "item": "Bearing Assembly", - "items": [ - { - "item_code": "Base Bearing Plate", - "qty": 1.0, - "rate": 15.0 - }, - { - "item_code": "Bearing Block", - "qty": 1.0, - "rate": 10.0 - }, - { - "item_code": "Bearing Collar", - "qty": 2.0, - "rate": 20.0 - }, - { - "item_code": "Bearing Pipe", - "qty": 1.0, - "rate": 15.0 - }, - { - "item_code": "Upper Bearing Plate", - "qty": 1.0, - "rate": 50.0 - } - ] - }, - { - "item": "Wind Mill A Series", - "items": [ - { - "item_code": "Base Bearing Plate", - "qty": 1.0, - "rate": 15.0 - }, - { - "item_code": "Base Plate", - "qty": 1.0, - "rate": 20.0 - }, - { - "item_code": "Bearing Block", - "qty": 1.0, - "rate": 10.0 - }, - { - "item_code": "Bearing Pipe", - "qty": 1.0, - "rate": 15.0 - }, - { - "item_code": "External Disc", - "qty": 1.0, - "rate": 45.0 - }, - { - "item_code": "Shaft", - "qty": 1.0, - "rate": 30.0 - }, - { - "item_code": "Wing Sheet", - "qty": 4.0, - "rate": 22.0 - } - ] - }, - { - "item": "Wind MIll C Series", - "items": [ - { - "item_code": "Base Plate", - "qty": 2.0, - "rate": 20.0 - }, - { - "item_code": "Internal Disc", - "qty": 1.0, - "rate": 33.0 - }, - { - "item_code": "External Disc", - "qty": 1.0, - "rate": 45.0 - }, - { - "item_code": "Bearing Assembly", - "qty": 1.0, - "rate": 130.0 - }, - { - "item_code": "Wing Sheet", - "qty": 3.0, - "rate": 22.0 - } - ] - }, - { - "item": "Wind Turbine-S", - "with_operations": 1, - "operations": [ - { - "operation": "Prepare Frame", - "time_in_mins": 30.0, - "workstation": "Drilling Machine 1" - }, - { - "operation": "Setup Fixtures", - "time_in_mins": 15.0, - "workstation": "Assembly Station 1" - }, - { - "operation": "Assembly Operation", - "time_in_mins": 30.0, - "workstation": "Assembly Station 1" - }, - { - "operation": "Wiring", - "time_in_mins": 20.0, - "workstation": "Assembly Station 1" - }, - { - "operation": "Testing", - "time_in_mins": 10.0, - "workstation": "Packing and Testing Station" - }, - { - "operation": "Packing", - "time_in_mins": 25.0, - "workstation": "Packing and Testing Station" - } - ], - "items": [ - { - "item_code": "Base Bearing Plate", - "qty": 1.0, - "rate": 15.0 - }, - { - "item_code": "Base Plate", - "qty": 1.0, - "rate": 20.0 - }, - { - "item_code": "Bearing Collar", - "qty": 1.0, - "rate": 20.0 - }, - { - "item_code": "Blade Rib", - "qty": 1.0, - "rate": 10.0 - }, - { - "item_code": "Shaft", - "qty": 1.0, - "rate": 30.0 - }, - { - "item_code": "Wing Sheet", - "qty": 2.0, - "rate": 22.0 - } - ] - }, - { - "item": "Base Plate", - "items": [ - { - "item_code": "Base Plate Un Painted", - "qty": 1.0, - "rate": 16.0 - } - ] - } -] \ No newline at end of file diff --git a/erpnext/demo/data/contact.json b/erpnext/demo/data/contact.json deleted file mode 100644 index 113b561ce5a..00000000000 --- a/erpnext/demo/data/contact.json +++ /dev/null @@ -1,164 +0,0 @@ -[ - { - "email_id": "JanVaclavik@example.com", - "first_name": "January", - "last_name": "V\u00e1clav\u00edk", - "links": [{"link_doctype": "Customer", "link_name": "Adaptas"}] - }, - { - "email_id": "ChidumagaTobeolisa@example.com", - "first_name": "Chidumaga", - "last_name": "Tobeolisa", - "links": [{"link_doctype": "Customer", "link_name": "Asian Fusion"}] - }, - { - "email_id": "JanaKubanova@example.com", - "first_name": "Jana", - "last_name": "Kub\u00e1\u0148ov\u00e1", - "links": [{"link_doctype": "Customer", "link_name": "Asian Junction"}] - }, - { - "email_id": "XuChaoXuan@example.com", - "first_name": "\u7d39\u8431", - "last_name": "\u4e8e", - "links": [{"link_doctype": "Customer", "link_name": "Big D Supermarkets"}] - }, - { - "email_id": "OzlemVerwijmeren@example.com", - "first_name": "\u00d6zlem", - "last_name": "Verwijmeren", - "links": [{"link_doctype": "Customer", "link_name": "Buttrey Food & Drug"}] - }, - { - "email_id": "HansRasmussen@example.com", - "first_name": "Hans", - "last_name": "Rasmussen", - "links": [{"link_doctype": "Customer", "link_name": "Chi-Chis"}] - }, - { - "email_id": "SatomiShigeki@example.com", - "first_name": "Satomi", - "last_name": "Shigeki", - "links": [{"link_doctype": "Customer", "link_name": "Choices"}] - }, - { - "email_id": "SimonVJessen@example.com", - "first_name": "Simon", - "last_name": "Jessen", - "links": [{"link_doctype": "Customer", "link_name": "Consumers and Consumers Express"}] - }, - { - "email_id": "NeguaranShahsaah@example.com", - "first_name": "\u0646\u06af\u0627\u0631\u06cc\u0646", - "last_name": "\u0634\u0627\u0647 \u0633\u06cc\u0627\u0647", - "links": [{"link_doctype": "Customer", "link_name": "Crafts Canada"}] - }, - { - "email_id": "Lom-AliBataev@example.com", - "first_name": "Lom-Ali", - "last_name": "Bataev", - "links": [{"link_doctype": "Customer", "link_name": "Endicott Shoes"}] - }, - { - "email_id": "VanNgocTien@example.com", - "first_name": "Ti\u00ean", - "last_name": "V\u0103n", - "links": [{"link_doctype": "Customer", "link_name": "Fayva"}] - }, - { - "email_id": "QuimeyOsorioRuelas@example.com", - "first_name": "Quimey", - "last_name": "Osorio", - "links": [{"link_doctype": "Customer", "link_name": "Intelacard"}] - }, - { - "email_id": "EdgardaSalcedoRaya@example.com", - "first_name": "Edgarda", - "last_name": "Salcedo", - "links": [{"link_doctype": "Customer", "link_name": "Landskip Yard Care"}] - }, - { - "email_id": "HafsteinnBjarnarsonar@example.com", - "first_name": "Hafsteinn", - "last_name": "Bjarnarsonar", - "links": [{"link_doctype": "Customer", "link_name": "Life Plan Counselling"}] - }, - { - "email_id": "\u0434\u0430\u043d\u0438\u0438\u043b@example.com", - "first_name": "\u0414\u0430\u043d\u0438\u0438\u043b", - "last_name": "\u041a\u043e\u043d\u043e\u0432\u0430\u043b\u043e\u0432", - "links": [{"link_doctype": "Customer", "link_name": "Mr Fables"}] - }, - { - "email_id": "SelmaMAndersen@example.com", - "first_name": "Selma", - "last_name": "Andersen", - "links": [{"link_doctype": "Customer", "link_name": "Nelson Brothers"}] - }, - { - "email_id": "LadislavKolaja@example.com", - "first_name": "Ladislav", - "last_name": "Kolaja", - "links": [{"link_doctype": "Customer", "link_name": "Netobill"}] - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "Helios Air"}], - "email_id": "TewoldeAbaalom@example.com", - "first_name": "Tewolde", - "last_name": "Abaalom" - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "Ks Merchandise"}], - "email_id": "LeilaFernandesRodrigues@example.com", - "first_name": "Leila", - "last_name": "Rodrigues" - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "HomeBase"}], - "email_id": "DmitryBulgakov@example.com", - "first_name": "Dmitry", - "last_name": "Bulgakov" - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "Scott Ties"}], - "email_id": "HaiducWhitfoot@example.com", - "first_name": "Haiduc", - "last_name": "Whitfoot" - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "Reliable Investments"}], - "email_id": "SesseljaPetursdottir@example.com", - "first_name": "Sesselja", - "last_name": "P\u00e9tursd\u00f3ttir" - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "Nan Duskin"}], - "email_id": "HajdarPignar@example.com", - "first_name": "Hajdar", - "last_name": "Pignar" - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "Rainbow Records"}], - "email_id": "GustavaLorenzo@example.com", - "first_name": "Gustava", - "last_name": "Lorenzo" - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "New World Realty"}], - "email_id": "BethanyWood@example.com", - "first_name": "Bethany", - "last_name": "Wood" - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "Asiatic Solutions"}], - "email_id": "GlorianaBrownlock@example.com", - "first_name": "Gloriana", - "last_name": "Brownlock" - }, - { - "links": [{"link_doctype": "Supplier", "link_name": "Eagle Hardware"}], - "email_id": "JensonFraser@gustr.com", - "first_name": "Jenson", - "last_name": "Fraser" - } -] \ No newline at end of file diff --git a/erpnext/demo/data/course.json b/erpnext/demo/data/course.json deleted file mode 100644 index 15728d51d3b..00000000000 --- a/erpnext/demo/data/course.json +++ /dev/null @@ -1,134 +0,0 @@ -[ - { - "doctype": "Course", - "course_name": "Communication Skiils", - "course_code": "BCA2040", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Object Oriented Programing - C++", - "course_code": "BCA2030", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Data Structures and Algorithm", - "course_code": "BCA2020", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Operating System", - "course_code": "BCA2010", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Digital Logic", - "course_code": "BCA1040", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Basic Mathematics", - "course_code": "BCA1030", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Programing in C", - "course_code": "BCA1020", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Fundamentals of IT & Programing", - "course_code": "BCA1010", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Microprocessor", - "course_code": "MCA4010", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Probability and Statistics", - "course_code": "MCA4020", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Programing in Java", - "course_code": "MCA4030", - "department": "Information Technology" - }, - { - "doctype": "Course", - "course_name": "Communication Skills", - "course_code": "BBA 101", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Organizational Behavior", - "course_code": "BBA 102", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Business Environment", - "course_code": "BBA 103", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Legal and Regulatory Framework", - "course_code": "BBA 301", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Human Resource Management", - "course_code": "BBA 302", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Advertising and Sales", - "course_code": "BBA 304", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Entrepreneurship Management", - "course_code": "BBA 505", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Visual Merchandising", - "course_code": "BBR 504", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Warehouse Management", - "course_code": "BBR 505", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Store Operations and Job Knowledge", - "course_code": "BBR 501", - "department": "Management Studies" - }, - { - "doctype": "Course", - "course_name": "Management Development and Skills", - "course_code": "BBA 602", - "department": "Management Studies" - } -] diff --git a/erpnext/demo/data/department.json b/erpnext/demo/data/department.json deleted file mode 100644 index f4355ba1e79..00000000000 --- a/erpnext/demo/data/department.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "doctype": "Department", - "department_name": "Information Technology" - }, - { - "doctype": "Department", - "department_name": "Physics" - }, - { - "doctype": "Department", - "department_name": "Chemistry" - }, - { - "doctype": "Department", - "department_name": "Biology" - }, - { - "doctype": "Department", - "department_name": "Commerce" - }, - { - "doctype": "Department", - "department_name": "English" - }, - { - "doctype": "Department", - "department_name": "Management Studies" - } -] \ No newline at end of file diff --git a/erpnext/demo/data/drug_list.json b/erpnext/demo/data/drug_list.json deleted file mode 100644 index 2ecd14f8af9..00000000000 --- a/erpnext/demo/data/drug_list.json +++ /dev/null @@ -1,5048 +0,0 @@ -[ - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Atocopherol", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Atocopherol", - "item_group": "Drug", - "item_name": "Atocopherol", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - - - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:16.577151", - "name": "Atocopherol", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Abacavir", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Abacavir", - "item_group": "Drug", - "item_name": "Abacavir", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - - - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:16.678257", - "name": "Abacavir", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Abciximab", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Abciximab", - "item_group": "Drug", - "item_name": "Abciximab", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:16.695413", - "name": "Abciximab", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Acacia", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Acacia", - "item_group": "Drug", - "item_name": "Acacia", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:16.797774", - "name": "Acacia", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Acamprosate", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Acamprosate", - "item_group": "Drug", - "item_name": "Acamprosate", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:16.826952", - "name": "Acamprosate", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Acarbose", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Acarbose", - "item_group": "Drug", - "item_name": "Acarbose", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:16.843890", - "name": "Acarbose", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Acebrofylline", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Acebrofylline", - "item_group": "Drug", - "item_name": "Acebrofylline", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:16.969984", - "name": "Acebrofylline", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Acebrofylline (SR)", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Acebrofylline (SR)", - "item_group": "Drug", - "item_name": "Acebrofylline (SR)", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:16.987354", - "name": "Acebrofylline (SR)", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Aceclofenac", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Aceclofenac", - "item_group": "Drug", - "item_name": "Aceclofenac", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.004369", - "name": "Aceclofenac", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Ash", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Ash", - "item_group": "Drug", - "item_name": "Ash", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.021192", - "name": "Ash", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Asparaginase", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Asparaginase", - "item_group": "Drug", - "item_name": "Asparaginase", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.038058", - "name": "Asparaginase", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Aspartame", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Aspartame", - "item_group": "Drug", - "item_name": "Aspartame", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.054463", - "name": "Aspartame", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Aspartic Acid", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Aspartic Acid", - "item_group": "Drug", - "item_name": "Aspartic Acid", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.071001", - "name": "Aspartic Acid", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Bleomycin", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Bleomycin", - "item_group": "Drug", - "item_name": "Bleomycin", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.087170", - "name": "Bleomycin", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Bleomycin Sulphate", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Bleomycin Sulphate", - "item_group": "Drug", - "item_name": "Bleomycin Sulphate", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.103691", - "name": "Bleomycin Sulphate", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Blue cap contains", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Blue cap contains", - "item_group": "Drug", - "item_name": "Blue cap contains", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.120040", - "name": "Blue cap contains", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Boran", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Boran", - "item_group": "Drug", - "item_name": "Boran", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.135964", - "name": "Boran", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Borax", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Borax", - "item_group": "Drug", - "item_name": "Borax", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.152575", - "name": "Borax", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlorbutanol", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlorbutanol", - "item_group": "Drug", - "item_name": "Chlorbutanol", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.168998", - "name": "Chlorbutanol", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlorbutol", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlorbutol", - "item_group": "Drug", - "item_name": "Chlorbutol", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.185316", - "name": "Chlorbutol", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlordiazepoxide", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlordiazepoxide", - "item_group": "Drug", - "item_name": "Chlordiazepoxide", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.208361", - "name": "Chlordiazepoxide", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlordiazepoxide and Clidinium Bromide", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlordiazepoxide and Clidinium Bromide", - "item_group": "Drug", - "item_name": "Chlordiazepoxide and Clidinium Bromide", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.224341", - "name": "Chlordiazepoxide and Clidinium Bromide", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlorhexidine", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlorhexidine", - "item_group": "Drug", - "item_name": "Chlorhexidine", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.240634", - "name": "Chlorhexidine", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlorhexidine 40%", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlorhexidine 40%", - "item_group": "Drug", - "item_name": "Chlorhexidine 40%", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.256922", - "name": "Chlorhexidine 40%", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlorhexidine Acetate", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlorhexidine Acetate", - "item_group": "Drug", - "item_name": "Chlorhexidine Acetate", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.274789", - "name": "Chlorhexidine Acetate", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlorhexidine Gluconate", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlorhexidine Gluconate", - "item_group": "Drug", - "item_name": "Chlorhexidine Gluconate", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.295371", - "name": "Chlorhexidine Gluconate", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlorhexidine HCL", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlorhexidine HCL", - "item_group": "Drug", - "item_name": "Chlorhexidine HCL", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.312916", - "name": "Chlorhexidine HCL", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chlorhexidine Hydrochloride", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chlorhexidine Hydrochloride", - "item_group": "Drug", - "item_name": "Chlorhexidine Hydrochloride", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.329570", - "name": "Chlorhexidine Hydrochloride", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Chloride", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Chloride", - "item_group": "Drug", - "item_name": "Chloride", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.346088", - "name": "Chloride", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Fosfomycin Tromethamine", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Fosfomycin Tromethamine", - "item_group": "Drug", - "item_name": "Fosfomycin Tromethamine", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.362777", - "name": "Fosfomycin Tromethamine", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Fosinopril", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Fosinopril", - "item_group": "Drug", - "item_name": "Fosinopril", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.379465", - "name": "Fosinopril", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Iodochlorhydroxyquinoline", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Iodochlorhydroxyquinoline", - "item_group": "Drug", - "item_name": "Iodochlorhydroxyquinoline", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.396068", - "name": "Iodochlorhydroxyquinoline", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Iodochlorohydroxyquinoline", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Iodochlorohydroxyquinoline", - "item_group": "Drug", - "item_name": "Iodochlorohydroxyquinoline", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.412734", - "name": "Iodochlorohydroxyquinoline", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Ipratropium", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Ipratropium", - "item_group": "Drug", - "item_name": "Ipratropium", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.429333", - "name": "Ipratropium", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Mebeverine hydrochloride", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Mebeverine hydrochloride", - "item_group": "Drug", - "item_name": "Mebeverine hydrochloride", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.445814", - "name": "Mebeverine hydrochloride", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Mecetronium ethylsulphate", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Mecetronium ethylsulphate", - "item_group": "Drug", - "item_name": "Mecetronium ethylsulphate", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.461696", - "name": "Mecetronium ethylsulphate", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Meclizine", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Meclizine", - "item_group": "Drug", - "item_name": "Meclizine", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.478020", - "name": "Meclizine", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Oxaprozin", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Oxaprozin", - "item_group": "Drug", - "item_name": "Oxaprozin", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - - - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.496221", - "name": "Oxaprozin", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Oxazepam", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Oxazepam", - "item_group": "Drug", - "item_name": "Oxazepam", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.511933", - "name": "Oxazepam", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Oxcarbazepine", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Oxcarbazepine", - "item_group": "Drug", - "item_name": "Oxcarbazepine", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.528472", - "name": "Oxcarbazepine", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Oxetacaine", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Oxetacaine", - "item_group": "Drug", - "item_name": "Oxetacaine", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.544177", - "name": "Oxetacaine", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Oxethazaine", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Oxethazaine", - "item_group": "Drug", - "item_name": "Oxethazaine", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.560193", - "name": "Oxethazaine", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Suxamethonium Chloride", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Suxamethonium Chloride", - "item_group": "Drug", - "item_name": "Suxamethonium Chloride", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.576447", - "name": "Suxamethonium Chloride", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Tacrolimus", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Tacrolimus", - "item_group": "Drug", - "item_name": "Tacrolimus", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.593481", - "name": "Tacrolimus", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Ubiquinol", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Ubiquinol", - "item_group": "Drug", - "item_name": "Ubiquinol", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.609930", - "name": "Ubiquinol", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Vitamin B12", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Vitamin B12", - "item_group": "Drug", - "item_name": "Vitamin B12", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.626225", - "name": "Vitamin B12", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Vitamin B1Hydrochloride", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Vitamin B1Hydrochloride", - "item_group": "Drug", - "item_name": "Vitamin B1Hydrochloride", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.642423", - "name": "Vitamin B1Hydrochloride", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Vitamin B1Monohydrate", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Vitamin B1Monohydrate", - "item_group": "Drug", - "item_name": "Vitamin B1Monohydrate", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.658946", - "name": "Vitamin B1Monohydrate", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Vitamin B2", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Vitamin B2", - "item_group": "Drug", - "item_name": "Vitamin B2", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.675234", - "name": "Vitamin B2", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Vitamin B3", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Vitamin B3", - "item_group": "Drug", - "item_name": "Vitamin B3", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.691598", - "name": "Vitamin B3", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Vitamin D4", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Vitamin D4", - "item_group": "Drug", - "item_name": "Vitamin D4", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.707840", - "name": "Vitamin D4", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Vitamin E", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Vitamin E", - "item_group": "Drug", - "item_name": "Vitamin E", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.723859", - "name": "Vitamin E", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Wheat Germ Oil", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Wheat Germ Oil", - "item_group": "Drug", - "item_name": "Wheat Germ Oil", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.739829", - "name": "Wheat Germ Oil", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Wheatgrass extr", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Wheatgrass extr", - "item_group": "Drug", - "item_name": "Wheatgrass extr", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.757695", - "name": "Wheatgrass extr", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Whey Protein", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Whey Protein", - "item_group": "Drug", - "item_name": "Whey Protein", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.774098", - "name": "Whey Protein", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Xylometazoline", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Xylometazoline", - "item_group": "Drug", - "item_name": "Xylometazoline", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.790224", - "name": "Xylometazoline", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Xylometazoline Hydrochloride", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Xylometazoline Hydrochloride", - "item_group": "Drug", - "item_name": "Xylometazoline Hydrochloride", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.806359", - "name": "Xylometazoline Hydrochloride", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Yeast", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Yeast", - "item_group": "Drug", - "item_name": "Yeast", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.823305", - "name": "Yeast", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Yellow Fever Vaccine", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Yellow Fever Vaccine", - "item_group": "Drug", - "item_name": "Yellow Fever Vaccine", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.840250", - "name": "Yellow Fever Vaccine", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Zafirlukast", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Zafirlukast", - "item_group": "Drug", - "item_name": "Zafirlukast", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.856856", - "name": "Zafirlukast", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Zaleplon", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Zaleplon", - "item_group": "Drug", - "item_name": "Zaleplon", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.873287", - "name": "Zaleplon", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Zaltoprofen", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Zaltoprofen", - "item_group": "Drug", - "item_name": "Zaltoprofen", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.889263", - "name": "Zaltoprofen", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - }, - { - "asset_category": null, - "attributes": [], - "barcode": null, - "brand": null, - "buying_cost_center": null, - "country_of_origin": null, - "create_new_batch": 0, - "customer_code": "", - "customer_items": [], - "customs_tariff_number": null, - "default_bom": null, - "default_material_request_type": null, - "default_supplier": null, - "default_warehouse": null, - "delivered_by_supplier": 0, - "description": "Zanamivir", - "disabled": 0, - "docstatus": 0, - "doctype": "Item", - "end_of_life": null, - "expense_account": null, - "gst_hsn_code": null, - "has_batch_no": 0, - "has_serial_no": 0, - "has_variants": 0, - "image": null, - "income_account": null, - "inspection_required_before_delivery": 0, - "inspection_required_before_purchase": 0, - "is_fixed_asset": 0, - "is_purchase_item": 1, - "is_sales_item": 1, - "is_stock_item": 1, - "is_sub_contracted_item": 0, - "item_code": "Zanamivir", - "item_group": "Drug", - "item_name": "Zanamivir", - "last_purchase_rate": 0.0, - "lead_time_days": 0, - "max_discount": 0.0, - "min_order_qty": 0.0, - "modified": "2017-07-06 12:53:17.905022", - "name": "Zanamivir", - "naming_series": null, - "net_weight": 0.0, - "opening_stock": 0.0, - "quality_parameters": [], - "reorder_levels": [], - "route": null, - "safety_stock": 0.0, - "selling_cost_center": null, - "serial_no_series": null, - "show_variant_in_website": 0, - "slideshow": null, - "standard_rate": 0.0, - "stock_uom": "Nos", - "supplier_items": [], - "taxes": [], - "thumbnail": null, - "tolerance": 0.0, - "uoms": [ - { - "conversion_factor": 1.0, - "uom": "Nos" - } - ], - "valuation_method": null, - "valuation_rate": 0.0, - "variant_based_on": null, - "variant_of": null, - "warranty_period": null, - "web_long_description": null, - "website_image": null, - "website_item_groups": [], - "website_specifications": [], - "website_warehouse": null, - "weight_uom": null, - "weightage": 0 - } -] diff --git a/erpnext/demo/data/employee.json b/erpnext/demo/data/employee.json deleted file mode 100644 index 2d2dbe894bc..00000000000 --- a/erpnext/demo/data/employee.json +++ /dev/null @@ -1,92 +0,0 @@ -[ - { - "date_of_birth": "1982-01-03", - "date_of_joining": "2001-10-10", - "employee_name": "Diana Prince", - "first_name": "Diana", - "last_name": "Prince", - "gender": "Female", - "user_id": "DianaPrince@example.com" - }, - { - "date_of_birth": "1959-02-03", - "date_of_joining": "1976-09-16", - "employee_name": "Zatanna Zatara", - "gender": "Female", - "user_id": "ZatannaZatara@example.com", - "first_name": "Zatanna", - "last_name": "Zatara" - }, - { - "date_of_birth": "1982-03-03", - "date_of_joining": "2000-06-16", - "employee_name": "Holly Granger", - "gender": "Female", - "user_id": "HollyGranger@example.com", - "first_name": "Holly", - "last_name": "Granger" - }, - { - "date_of_birth": "1945-04-04", - "date_of_joining": "1969-07-01", - "employee_name": "Neptunia Aquaria", - "gender": "Female", - "user_id": "NeptuniaAquaria@example.com", - "first_name": "Neptunia", - "last_name": "Aquaria" - }, - { - "date_of_birth": "1978-05-03", - "date_of_joining": "1999-12-24", - "employee_name": "Arthur Curry", - "gender": "Male", - "user_id": "ArthurCurry@example.com", - "first_name": "Arthur", - "last_name": "Curry" - }, - { - "date_of_birth": "1964-06-03", - "date_of_joining": "1981-08-05", - "employee_name": "Thalia Al Ghul", - "gender": "Female", - "user_id": "ThaliaAlGhul@example.com", - "first_name": "Thalia", - "last_name": "Al Ghul" - }, - { - "date_of_birth": "1982-07-03", - "date_of_joining": "2006-06-10", - "employee_name": "Maxwell Lord", - "gender": "Male", - "user_id": "MaxwellLord@example.com", - "first_name": "Maxwell", - "last_name": "Lord" - }, - { - "date_of_birth": "1969-08-03", - "date_of_joining": "1993-10-21", - "employee_name": "Grace Choi", - "gender": "Female", - "user_id": "GraceChoi@example.com", - "first_name": "Grace", - "last_name": "Choi" - }, - { - "date_of_birth": "1982-09-03", - "date_of_joining": "2005-09-06", - "employee_name": "Vandal Savage", - "gender": "Male", - "user_id": "VandalSavage@example.com", - "first_name": "Vandal", - "last_name": "Savage" - }, - { - "date_of_birth": "1985-10-03", - "date_of_joining": "2007-12-25", - "employee_name": "Caitlin Snow", - "gender": "Female", - "user_id": "CaitlinSnow@example.com", - "first_name": "Caitlin", - "last_name": "Snow" - } -] \ No newline at end of file diff --git a/erpnext/demo/data/grading_scale.json b/erpnext/demo/data/grading_scale.json deleted file mode 100644 index 07609197c45..00000000000 --- a/erpnext/demo/data/grading_scale.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "doctype": "Grading Scale", - "grading_scale_name": "Standard Grading", - "description": "Standard Grading Scale", - "intervals": [ - {"threshold": 100.0, "grade_code": "A", "grade_description": "Excellent"}, - {"threshold": 89.9, "grade_code": "B+", "grade_description": "Close to Excellence"}, - {"threshold": 80.0, "grade_code": "B", "grade_description": "Good"}, - {"threshold": 69.9, "grade_code": "C+", "grade_description": "Almost Good"}, - {"threshold": 60.0, "grade_code": "C", "grade_description": "Average"}, - {"threshold": 50.0, "grade_code": "D+", "grade_description": "Have to Work"}, - {"threshold": 40.0, "grade_code": "D", "grade_description": "Not met Baseline Expectations"}, - {"threshold": 0.0, "grade_code": "F", "grade_description": "Have to work a lot"} - ] - } -] \ No newline at end of file diff --git a/erpnext/demo/data/instructor.json b/erpnext/demo/data/instructor.json deleted file mode 100644 index a25d16304d7..00000000000 --- a/erpnext/demo/data/instructor.json +++ /dev/null @@ -1,128 +0,0 @@ -[ - { - "doctype": "Instructor", - "instructor_name": "Eddie Jessup", - "naming_series": "INS/", - "department": "Information Technology" - }, - { - "doctype": "Instructor", - "instructor_name": "William Dyer", - "naming_series": "INS/", - "department": "Information Technology" - }, - { - "doctype": "Instructor", - "instructor_name": "Alastor Moody", - "naming_series": "INS/", - "department": "Information Technology" - }, - { - "doctype": "Instructor", - "instructor_name": "Charles Xavier", - "naming_series": "INS/", - "department": "Information Technology" - }, - { - "doctype": "Instructor", - "instructor_name": "Cuthbert Calculus", - "naming_series": "INS/", - "department": "Information Technology" - }, - { - "doctype": "Instructor", - "instructor_name": "Reed Richards", - "naming_series": "INS/", - "department": "Information Technology" - }, - { - "doctype": "Instructor", - "instructor_name": "Urban Chronotis", - "naming_series": "INS/", - "department": "Physics" - }, - { - "doctype": "Instructor", - "instructor_name": "River Song", - "naming_series": "INS/", - "department": "Physics" - }, - { - "doctype": "Instructor", - "instructor_name": "Yana", - "naming_series": "INS/", - "department": "Physics" - }, - { - "doctype": "Instructor", - "instructor_name": "Neil Lasrado", - "naming_series": "INS/", - "department": "Information Technology" - }, - { - "doctype": "Instructor", - "instructor_name": "Deepshi Garg", - "naming_series": "INS/", - "department": "Chemistry" - }, - { - "doctype": "Instructor", - "instructor_name": "Shubham Saxena", - "naming_series": "INS/", - "department": "Physics" - }, - { - "doctype": "Instructor", - "instructor_name": "Rushabh Mehta", - "naming_series": "INS/", - "department": "Information Technology" - }, - { - "doctype": "Instructor", - "instructor_name": "Umari Syed", - "naming_series": "INS/", - "department": "Chemistry" - }, - { - "doctype": "Instructor", - "instructor_name": "Aman Singh", - "naming_series": "INS/", - "department": "Physics" - }, - { - "doctype": "Instructor", - "instructor_name": "Nabin", - "naming_series": "INS/", - "department": "Chemistry" - }, - { - "doctype": "Instructor", - "instructor_name": "Kanchan Chauhan", - "naming_series": "INS/", - "department": "Information Technology" - }, - { - "doctype": "Instructor", - "instructor_name": "Valmik Jangla", - "naming_series": "INS/", - "department": "Chemistry" - }, - { - "doctype": "Instructor", - "instructor_name": "Amit Jain", - "naming_series": "INS/", - "department": "Physics" - }, - { - "doctype": "Instructor", - "instructor_name": "Shreyas P", - "naming_series": "INS/", - "department": "Chemistry" - }, - { - "doctype": "Instructor", - "instructor_name": "Rohit", - "naming_series": "INS/", - "department": "Information Technology" - } -] \ No newline at end of file diff --git a/erpnext/demo/data/item.json b/erpnext/demo/data/item.json deleted file mode 100644 index 1d4ed343be6..00000000000 --- a/erpnext/demo/data/item.json +++ /dev/null @@ -1,493 +0,0 @@ -[ - { - "item_defaults": [ - { - "default_supplier": "Asiatic Solutions", - "default_warehouse": "Stores" - } - ], - "description": "For Upper Bearing", - "image": "/assets/erpnext_demo/images/disc.png", - "item_code": "Disc Collars", - "item_group": "Raw Material", - "item_name": "Disc Collars" - }, - { - "item_defaults": [ - { - "default_supplier": "Nan Duskin", - "default_warehouse": "Stores" - } - ], - "description": "CAST IRON, MCMASTER PART NO. 3710T13", - "image": "/assets/erpnext_demo/images/bearing.jpg", - "item_code": "Bearing Block", - "item_group": "Raw Material", - "item_name": "Bearing Block" - }, - { - "item_defaults": [ - { - "default_supplier": null, - "default_warehouse": "Finished Goods" - } - ], - "description": "Wind Mill C Series for Commercial Use 18ft", - "image": "/assets/erpnext_demo/images/wind-turbine-2.png", - "item_code": "Wind MIll C Series", - "item_group": "Products", - "item_name": "Wind MIll C Series" - }, - { - "item_defaults": [ - { - "default_supplier": null, - "default_warehouse": "Finished Goods" - } - ], - "description": "Wind Mill A Series for Home Use 9ft", - "image": "/assets/erpnext_demo/images/wind-turbine.png", - "item_code": "Wind Mill A Series", - "item_group": "Products", - "item_name": "Wind Mill A Series" - }, - { - "item_defaults": [ - { - "default_supplier": null, - "default_warehouse": "Finished Goods" - } - ], - "description": "Small Wind Turbine for Home Use\n\n\n", - "image": "/assets/erpnext_demo/images/wind-turbine-1.jpg", - "item_code": "Wind Turbine", - "item_group": "Products", - "item_name": "Wind Turbine", - "has_variants": 1, - "has_serial_no": 1, - "attributes": [ - { - "attribute": "Size" - } - ] - }, - { - "item_defaults": [ - { - "default_supplier": "HomeBase", - "default_warehouse": "Stores" - } - ], - "description": "1.5 in. Diameter x 36 in. Mild Steel Tubing", - "image": null, - "item_code": "Bearing Pipe", - "item_group": "Raw Material", - "item_name": "Bearing Pipe" - }, - { - "item_defaults": [ - { - "default_supplier": "New World Realty", - "default_warehouse": "Stores" - } - ], - "description": "1/32 in. x 24 in. x 47 in. HDPE Opaque Sheet", - "image": null, - "item_code": "Wing Sheet", - "item_group": "Raw Material", - "item_name": "Wing Sheet" - }, - { - "item_defaults": [ - { - "default_supplier": "Eagle Hardware", - "default_warehouse": "Stores" - } - ], - "description": "3/16 in. x 6 in. x 6 in. Low Carbon Steel Plate", - "image": null, - "item_code": "Upper Bearing Plate", - "item_group": "Raw Material", - "item_name": "Upper Bearing Plate" - }, - { - "item_defaults": [ - { - "default_supplier": "Asiatic Solutions", - "default_warehouse": "Stores" - } - ], - "description": "Bearing Assembly", - "image": null, - "item_code": "Bearing Assembly", - "item_group": "Sub Assemblies", - "item_name": "Bearing Assembly" - }, - { - "item_defaults": [ - { - "default_supplier": "HomeBase", - "default_warehouse": "Stores" - } - ], - "description": "3/4 in. x 2 ft. x 4 ft. Pine Plywood", - "image": null, - "item_code": "Base Plate", - "item_group": "Raw Material", - "item_name": "Base Plate", - "is_sub_contracted_item": 1 - }, - { - "item_defaults": [ - { - "default_supplier": "Scott Ties", - "default_warehouse": "Stores" - } - ], - "description": "N/A", - "image": null, - "item_code": "Stand", - "item_group": "Raw Material", - "item_name": "Stand" - }, - { - "item_defaults": [ - { - "default_supplier": "Eagle Hardware", - "default_warehouse": "Stores" - } - ], - "description": "1 in. x 3 in. x 1 ft. Multipurpose Al Alloy Bar", - "image": null, - "item_code": "Bearing Collar", - "item_group": "Raw Material", - "item_name": "Bearing Collar" - }, - { - "item_defaults": [ - { - "default_supplier": "Eagle Hardware", - "default_warehouse": "Stores" - } - ], - "description": "1/4 in. x 6 in. x 6 in. Mild Steel Plate", - "image": null, - "item_code": "Base Bearing Plate", - "item_group": "Raw Material", - "item_name": "Base Bearing Plate" - }, - { - "item_defaults": [ - { - "default_supplier": "HomeBase", - "default_warehouse": "Stores" - } - ], - "description": "15/32 in. x 4 ft. x 8 ft. 3-Ply Rtd Sheathing", - "image": null, - "item_code": "External Disc", - "item_group": "Raw Material", - "item_name": "External Disc" - }, - { - "item_defaults": [ - { - "default_supplier": "Eagle Hardware", - "default_warehouse": "Stores" - } - ], - "description": "1.25 in. Diameter x 6 ft. Mild Steel Tubing", - "image": null, - "item_code": "Shaft", - "item_group": "Raw Material", - "item_name": "Shaft" - }, - { - "item_defaults": [ - { - "default_supplier": "Ks Merchandise", - "default_warehouse": "Stores" - } - ], - "description": "1/2 in. x 2 ft. x 4 ft. Pine Plywood", - "image": null, - "item_code": "Blade Rib", - "item_group": "Raw Material", - "item_name": "Blade Rib" - }, - { - "item_defaults": [ - { - "default_supplier": "HomeBase", - "default_warehouse": "Stores" - } - ], - "description": "For Bearing Collar", - "image": null, - "item_code": "Internal Disc", - "item_group": "Raw Material", - "item_name": "Internal Disc" - }, - { - "item_defaults": [ - { - "default_supplier": null, - "default_warehouse": "Finished Goods" - } - ], - "description": "Small Wind Turbine for Home Use\n\n\n\n

Size: Small

", - "image": "/assets/erpnext_demo/images/wind-turbine-1.jpg", - "item_code": "Wind Turbine-S", - "item_group": "Products", - "item_name": "Wind Turbine-S", - "variant_of": "Wind Turbine", - "valuation_rate": 300, - "attributes": [ - { - "attribute": "Size", - "attribute_value": "Small" - } - ] - }, - { - "item_defaults": [ - { - "default_supplier": null, - "default_warehouse": "Finished Goods" - } - ], - "description": "Small Wind Turbine for Home Use\n\n\n\n

Size: Medium

", - "image": "/assets/erpnext_demo/images/wind-turbine-1.jpg", - "item_code": "Wind Turbine-M", - "item_group": "Products", - "item_name": "Wind Turbine-M", - "variant_of": "Wind Turbine", - "valuation_rate": 300, - "attributes": [ - { - "attribute": "Size", - "attribute_value": "Medium" - } - ] - }, - { - "item_defaults": [ - { - "default_supplier": null, - "default_warehouse": "Finished Goods" - } - ], - "description": "Small Wind Turbine for Home Use\n\n\n\n

Size: Large

", - "image": "/assets/erpnext_demo/images/wind-turbine-1.jpg", - "item_code": "Wind Turbine-L", - "item_group": "Products", - "item_name": "Wind Turbine-L", - "variant_of": "Wind Turbine", - "valuation_rate": 300, - "attributes": [ - { - "attribute": "Size", - "attribute_value": "Large" - } - ] - }, - { - "is_stock_item": 0, - "description": "Wind Mill A Series with Spare Bearing", - "item_code": "Wind Mill A Series with Spare Bearing", - "item_group": "Products", - "item_name": "Wind Mill A Series with Spare Bearing" - }, - { - "item_defaults": [ - { - "default_supplier": "HomeBase", - "default_warehouse": "Stores" - } - ], - "description": "3/4 in. x 2 ft. x 4 ft. Pine Plywood", - "image": null, - "item_code": "Base Plate Un Painted", - "item_group": "Raw Material", - "item_name": "Base Plate Un Painted" - }, - { - "is_fixed_asset": 1, - "asset_category": "Furnitures", - "is_stock_item": 0, - "description": "Table", - "item_code": "Table", - "item_name": "Table", - "item_group": "Products" - }, - { - "is_fixed_asset": 1, - "asset_category": "Furnitures", - "is_stock_item": 0, - "description": "Chair", - "item_code": "Chair", - "item_name": "Chair", - "item_group": "Products" - }, - { - "is_fixed_asset": 1, - "asset_category": "Electronic Equipments", - "is_stock_item": 0, - "description": "Computer", - "item_code": "Computer", - "item_name": "Computer", - "item_group": "Products" - }, - { - "is_fixed_asset": 1, - "asset_category": "Electronic Equipments", - "is_stock_item": 0, - "description": "Mobile", - "item_code": "Mobile", - "item_name": "Mobile", - "item_group": "Products" - }, - { - "is_fixed_asset": 1, - "asset_category": "Softwares", - "is_stock_item": 0, - "description": "ERP", - "item_code": "ERP", - "item_name": "ERP", - "item_group": "All Item Groups" - }, - { - "is_fixed_asset": 1, - "asset_category": "Softwares", - "is_stock_item": 0, - "description": "Autocad", - "item_code": "Autocad", - "item_name": "Autocad", - "item_group": "All Item Groups" - }, - { - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "valuation_rate": 200, - "item_defaults": [ - { - "default_warehouse": "Stores" - } - ], - "description": "Corrugated Box", - "item_code": "Corrugated Box", - "item_name": "Corrugated Box", - "item_group": "All Item Groups" - }, - { - "item_defaults": [ - { - "default_warehouse": "Finished Goods" - } - ], - "is_stock_item": 1, - "description": "OnePlus 6", - "item_code": "OnePlus 6", - "item_name": "OnePlus 6", - "item_group": "Products", - "domain": "Retail" - }, - { - "item_defaults": [ - { - "default_warehouse": "Finished Goods" - } - ], - "is_stock_item": 1, - "description": "OnePlus 6T", - "item_code": "OnePlus 6T", - "item_name": "OnePlus 6T", - "item_group": "Products", - "domain": "Retail" - }, - { - "item_defaults": [ - { - "default_warehouse": "Finished Goods" - } - ], - "is_stock_item": 1, - "description": "Xiaomi Poco F1", - "item_code": "Xiaomi Poco F1", - "item_name": "Xiaomi Poco F1", - "item_group": "Products", - "domain": "Retail" - }, - { - "item_defaults": [ - { - "default_warehouse": "Finished Goods" - } - ], - "is_stock_item": 1, - "description": "Iphone XS", - "item_code": "Iphone XS", - "item_name": "Iphone XS", - "item_group": "Products", - "domain": "Retail" - }, - { - "item_defaults": [ - { - "default_warehouse": "Finished Goods" - } - ], - "is_stock_item": 1, - "description": "Samsung Galaxy S9", - "item_code": "Samsung Galaxy S9", - "item_name": "Samsung Galaxy S9", - "item_group": "Products", - "domain": "Retail" - }, - { - "item_defaults": [ - { - "default_warehouse": "Finished Goods" - } - ], - "is_stock_item": 1, - "description": "Sony Bluetooth Headphone", - "item_code": "Sony Bluetooth Headphone", - "item_name": "Sony Bluetooth Headphone", - "item_group": "Products", - "domain": "Retail" - }, - { - "is_stock_item": 0, - "description": "Samsung Phone Repair", - "item_code": "Samsung Phone Repair", - "item_name": "Samsung Phone Repair", - "item_group": "Services", - "domain": "Retail" - }, - { - "is_stock_item": 0, - "description": "OnePlus Phone Repair", - "item_code": "OnePlus Phone Repair", - "item_name": "OnePlus Phone Repair", - "item_group": "Services", - "domain": "Retail" - }, - { - "is_stock_item": 0, - "description": "Xiaomi Phone Repair", - "item_code": "Xiaomi Phone Repair", - "item_name": "Xiaomi Phone Repair", - "item_group": "Services", - "domain": "Retail" - }, - { - "is_stock_item": 0, - "description": "Apple Phone Repair", - "item_code": "Apple Phone Repair", - "item_name": "Apple Phone Repair", - "item_group": "Services", - "domain": "Retail" - } -] \ No newline at end of file diff --git a/erpnext/demo/data/item_education.json b/erpnext/demo/data/item_education.json deleted file mode 100644 index 40e4701596a..00000000000 --- a/erpnext/demo/data/item_education.json +++ /dev/null @@ -1,137 +0,0 @@ -[ - { - "default_supplier": "Asiatic Solutions", - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "item_code": "Books", - "item_group": "Raw Material", - "item_name": "Books" - }, - { - "default_supplier": "HomeBase", - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "item_code": "Pencil", - "item_group": "Raw Material", - "item_name": "Pencil" - }, - { - "default_supplier": "New World Realty", - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "item_code": "Tables", - "item_group": "Raw Material", - "item_name": "Tables" - }, - { - "default_supplier": "Eagle Hardware", - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "item_code": "Chair", - "item_group": "Raw Material", - "item_name": "Chair" - }, - { - "default_supplier": "Asiatic Solutions", - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "item_code": "Black Board", - "item_group": "Sub Assemblies", - "item_name": "Black Board" - }, - { - "default_supplier": "HomeBase", - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "item_code": "Chalk", - "item_group": "Raw Material", - "item_name": "Chalk" - }, - { - "default_supplier": "HomeBase", - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "item_code": "Notepad", - "item_group": "Raw Material", - "item_name": "Notepad" - }, - { - "default_supplier": "Ks Merchandise", - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "item_code": "Uniform", - "item_group": "Raw Material", - "item_name": "Uniform" - }, - { - "is_stock_item": 0, - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "description": "Computer", - "item_code": "Computer", - "item_name": "Computer", - "item_group": "Products" - }, - { - "is_stock_item": 0, - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "description": "Mobile", - "item_code": "Mobile", - "item_name": "Mobile", - "item_group": "Products" - }, - { - "is_stock_item": 0, - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "description": "ERP", - "item_code": "ERP", - "item_name": "ERP", - "item_group": "All Item Groups" - }, - { - "is_stock_item": 0, - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "description": "Autocad", - "item_code": "Autocad", - "item_name": "Autocad", - "item_group": "All Item Groups" - }, - { - "item_defaults": [{ - "default_warehouse": "Stores", - "company": "Whitmore College" - }], - "item_code": "Service", - "item_group": "Services", - "item_name": "Service", - "has_variants": 0, - "is_stock_item": 0 - } -] \ No newline at end of file diff --git a/erpnext/demo/data/lead.json b/erpnext/demo/data/lead.json deleted file mode 100644 index ff788778977..00000000000 --- a/erpnext/demo/data/lead.json +++ /dev/null @@ -1,127 +0,0 @@ -[ - { - "company_name": "Zany Brainy", - "email_id": "MartLakeman@example.com", - "lead_name": "Mart Lakeman" - }, - { - "company_name": "Patterson-Fletcher", - "email_id": "SagaLundqvist@example.com", - "lead_name": "Saga Lundqvist" - }, - { - "company_name": "Griff's Hamburgers", - "email_id": "AdnaSjoberg@example.com", - "lead_name": "Adna Sj\u00f6berg" - }, - { - "company_name": "Rhodes Furniture", - "email_id": "IdaDSvendsen@example.com", - "lead_name": "Ida Svendsen" - }, - { - "company_name": "Burger Chef", - "email_id": "EmppuHameenniemi@example.com", - "lead_name": "Emppu H\u00e4meenniemi" - }, - { - "company_name": "Stratabiz", - "email_id": "EugenioPisano@example.com", - "lead_name": "Eugenio Pisano" - }, - { - "company_name": "Home Quarters Warehouse", - "email_id": "SemharHagos@example.com", - "lead_name": "Semhar Hagos" - }, - { - "company_name": "Enviro Architectural Designs", - "email_id": "BranimiraIvankovic@example.com", - "lead_name": "Branimira Ivankovi\u0107" - }, - { - "company_name": "Ideal Garden Management", - "email_id": "ShellyLFields@example.com", - "lead_name": "Shelly Fields" - }, - { - "company_name": "Listen Up", - "email_id": "LeoMikulic@example.com", - "lead_name": "Leo Mikuli\u0107" - }, - { - "company_name": "I. Magnin", - "email_id": "DenisaJarosova@example.com", - "lead_name": "Denisa Jaro\u0161ov\u00e1" - }, - { - "company_name": "First Rate Choice", - "email_id": "JanekRutkowski@example.com", - "lead_name": "Janek Rutkowski" - }, - { - "company_name": "Multi Tech Development", - "email_id": "mm@example.com", - "lead_name": "\u7f8e\u6708 \u5b87\u85e4" - }, - { - "company_name": "National Auto Parts", - "email_id": "dd@example.com", - "lead_name": "\u0414\u0430\u043d\u0438\u0438\u043b \u0410\u0444\u0430\u043d\u0430\u0441\u044c\u0435\u0432" - }, - { - "company_name": "Integra Investment Plan", - "email_id": "ZorislavPetkovic@example.com", - "lead_name": "Zorislav Petkovi\u0107" - }, - { - "company_name": "The Lawn Guru", - "email_id": "NanaoNiwa@example.com", - "lead_name": "Nanao Niwa" - }, - { - "company_name": "Buena Vista Realty Service", - "email_id": "HreiarJorundsson@example.com", - "lead_name": "Hrei\u00f0ar J\u00f6rundsson" - }, - { - "company_name": "Bountiful Harvest Health Food Store", - "email_id": "ChuThiBichLai@example.com", - "lead_name": "Lai Chu" - }, - { - "company_name": "P. Samuels Men's Clothiers", - "email_id": "VictorAksakov@example.com", - "lead_name": "Victor Aksakov" - }, - { - "company_name": "Vinyl Fever", - "email_id": "SaidalimBisliev@example.com", - "lead_name": "Saidalim Bisliev" - }, - { - "company_name": "Garden Master", - "email_id": "TotteJakobsson@example.com", - "lead_name": "Totte Jakobsson" - }, - { - "company_name": "Big Apple", - "email_id": "NanaArmasRobles@example.com", - "lead_name": "Nan\u00e1 Armas" - }, - { - "company_name": "Monk House Sales", - "email_id": "WalerianDuda@example.com", - "lead_name": "Walerian Duda" - }, - { - "company_name": "ManCharm", - "email_id": "Moarimikashi@example.com", - "lead_name": "Moarimikashi" - }, - { - "company_name": "Custom Lawn Care", - "email_id": "DobromilDabrowski@example.com", - "lead_name": "Dobromi\u0142 D\u0105browski" - } -] \ No newline at end of file diff --git a/erpnext/demo/data/location.json b/erpnext/demo/data/location.json deleted file mode 100644 index b521aa08c48..00000000000 --- a/erpnext/demo/data/location.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "location_name": "Main Location", - "latitude": 40.0, - "longitude": 20.0 - }, - { - "location_name": "Avg Location", - "latitude": 63.0, - "longitude": 99.3 - }, - { - "location_name": "Zany Location", - "latitude": 47.5, - "longitude": 10.0 - }, - { - "location_name": "Fletcher Location", - "latitude": 100.90, - "longitude": 80 - } -] \ No newline at end of file diff --git a/erpnext/demo/data/operation.json b/erpnext/demo/data/operation.json deleted file mode 100644 index 47f26d1d521..00000000000 --- a/erpnext/demo/data/operation.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "description": "Setup Fixtures for Assembly", - "name": "Setup Fixtures", - "workstation": "Assembly Station 1" - }, - { - "description": "Assemble Unit as per Standard Operating Procedures", - "name": "Assembly Operation", - "workstation": "Assembly Station 1" - }, - { - "description": "Final Testing Checklist", - "name": "Testing", - "workstation": "Packing and Testing Station" - }, - { - "description": "Final Packing and add Instructions", - "name": "Packing", - "workstation": "Packing and Testing Station" - }, - { - "description": "Prepare frame for assembly", - "name": "Prepare Frame", - "workstation": "Drilling Machine 1" - }, - { - "description": "Connect wires", - "name": "Wiring", - "workstation": "Assembly Station 1" - } -] \ No newline at end of file diff --git a/erpnext/demo/data/patient.json b/erpnext/demo/data/patient.json deleted file mode 100644 index 6d95a202021..00000000000 --- a/erpnext/demo/data/patient.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "patient_name": "lila", - "gender": "Female" - }, - { - "patient_name": "charline", - "gender": "Female" - }, - { - "patient_name": "soren", - "last_name": "le gall", - "gender": "Male" - }, - { - "patient_name": "fanny", - "gender": "Female" - }, - { - "patient_name": "julie", - "gender": "Female" - }, - { - "patient_name": "louka", - "gender": "Male" - } -] diff --git a/erpnext/demo/data/practitioner.json b/erpnext/demo/data/practitioner.json deleted file mode 100644 index 39c960fcd7e..00000000000 --- a/erpnext/demo/data/practitioner.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "doctype": "Healthcare Practitioner", - "first_name": "Eddie Jessup", - "department": "Pathology" - }, - { - "doctype": "Healthcare Practitioner", - "first_name": "Deepshi Garg", - "department": "ENT" - }, - { - "doctype": "Healthcare Practitioner", - "first_name": "Amit Jain", - "department": "Microbiology" - } -] diff --git a/erpnext/demo/data/program.json b/erpnext/demo/data/program.json deleted file mode 100644 index 9c2ec77a4b9..00000000000 --- a/erpnext/demo/data/program.json +++ /dev/null @@ -1,46 +0,0 @@ -[ - { - "doctype": "Program", - "name": "MCA", - "program_name": "Masters of Computer Applications", - "program_code": "MCA", - "department": "Information Technology", - "courses": [ - { "course": "MCA4010" }, - { "course": "MCA4020" }, - { "course": "MCA4030" } - ] - }, - { - "doctype": "Program", - "name": "BCA", - "program_name": "Bachelor of Computer Applications", - "program_code": "BCA", - "department": "Information Technology", - "courses": [ - { "course": "BCA2030" }, - { "course": "BCA1030" }, - { "course": "BCA2020" }, - { "course": "BCA1040" }, - { "course": "BCA1010" }, - { "course": "BCA2010" }, - { "course": "BCA1020" } - ] - }, - { - "doctype": "Program", - "name": "BBA", - "program_name": "Bachelor of Business Administration", - "program_code": "BBA", - "department": "Management Studies", - "courses": [ - { "course": "BBA 101" }, - { "course": "BBA 102" }, - { "course": "BBA 103" }, - { "course": "BBA 301" }, - { "course": "BBA 302" }, - { "course": "BBA 304" }, - { "course": "BBA 505" } - ] - } -] \ No newline at end of file diff --git a/erpnext/demo/data/random_student_data.json b/erpnext/demo/data/random_student_data.json deleted file mode 100644 index babcc715760..00000000000 --- a/erpnext/demo/data/random_student_data.json +++ /dev/null @@ -1,1604 +0,0 @@ -[ -{ -"first_name": "amanda", -"last_name": "edwards", -"image": "https://randomuser.me/api/portraits/women/55.jpg", -"gender": "Female" -}, -{ -"first_name": "abbie", -"last_name": "johnston", -"image": "https://randomuser.me/api/portraits/women/46.jpg", -"gender": "Female" -}, -{ -"first_name": "heather", -"last_name": "nelson", -"image": "https://randomuser.me/api/portraits/women/13.jpg", -"gender": "Female" -}, -{ -"first_name": "maxwell", -"last_name": "gilbert", -"image": "https://randomuser.me/api/portraits/men/56.jpg", -"gender": "Male" -}, -{ -"first_name": "molly", -"last_name": "ramirez", -"image": "https://randomuser.me/api/portraits/women/71.jpg", -"gender": "Female" -}, -{ -"first_name": "ian", -"last_name": "barrett", -"image": "https://randomuser.me/api/portraits/men/68.jpg", -"gender": "Male" -}, -{ -"first_name": "kim", -"last_name": "hudson", -"image": "https://randomuser.me/api/portraits/women/53.jpg", -"gender": "Female" -}, -{ -"first_name": "bruce", -"last_name": "murray", -"image": "https://randomuser.me/api/portraits/men/59.jpg", -"gender": "Male" -}, -{ -"first_name": "henry", -"last_name": "powell", -"image": "https://randomuser.me/api/portraits/men/88.jpg", -"gender": "Male" -}, -{ -"first_name": "chris", -"last_name": "foster", -"image": "https://randomuser.me/api/portraits/men/5.jpg", -"gender": "Male" -}, -{ -"first_name": "billy", -"last_name": "kim", -"image": "https://randomuser.me/api/portraits/men/91.jpg", -"gender": "Male" -}, -{ -"first_name": "samuel", -"last_name": "harper", -"image": "https://randomuser.me/api/portraits/men/56.jpg", -"gender": "Male" -}, -{ -"first_name": "jayden", -"last_name": "kelly", -"image": "https://randomuser.me/api/portraits/men/31.jpg", -"gender": "Male" -}, -{ -"first_name": "grace", -"last_name": "berry", -"image": "https://randomuser.me/api/portraits/women/69.jpg", -"gender": "Female" -}, -{ -"first_name": "ronnie", -"last_name": "nelson", -"image": "https://randomuser.me/api/portraits/men/83.jpg", -"gender": "Male" -}, -{ -"first_name": "harvey", -"last_name": "harper", -"image": "https://randomuser.me/api/portraits/men/68.jpg", -"gender": "Male" -}, -{ -"first_name": "maya", -"last_name": "fernandez", -"image": "https://randomuser.me/api/portraits/women/79.jpg", -"gender": "Female" -}, -{ -"first_name": "faith", -"last_name": "lewis", -"image": "https://randomuser.me/api/portraits/women/84.jpg", -"gender": "Female" -}, -{ -"first_name": "kirk", -"last_name": "macrae", -"image": "https://randomuser.me/api/portraits/men/13.jpg", -"gender": "Male" -}, -{ -"first_name": "tracy", -"last_name": "holt", -"image": "https://randomuser.me/api/portraits/women/18.jpg", -"gender": "Female" -}, -{ -"first_name": "mandy", -"last_name": "dean", -"image": "https://randomuser.me/api/portraits/women/0.jpg", -"gender": "Female" -}, -{ -"first_name": "sam", -"last_name": "dunn", -"image": "https://randomuser.me/api/portraits/women/12.jpg", -"gender": "Female" -}, -{ -"first_name": "zoe", -"last_name": "fleming", -"image": "https://randomuser.me/api/portraits/women/9.jpg", -"gender": "Female" -}, -{ -"first_name": "jeffrey", -"last_name": "stewart", -"image": "https://randomuser.me/api/portraits/men/56.jpg", -"gender": "Male" -}, -{ -"first_name": "dick", -"last_name": "ryan", -"image": "https://randomuser.me/api/portraits/men/63.jpg", -"gender": "Male" -}, -{ -"first_name": "carl", -"last_name": "neal", -"image": "https://randomuser.me/api/portraits/men/41.jpg", -"gender": "Male" -}, -{ -"first_name": "scarlett", -"last_name": "ruiz", -"image": "https://randomuser.me/api/portraits/women/24.jpg", -"gender": "Female" -}, -{ -"first_name": "rene", -"last_name": "hughes", -"image": "https://randomuser.me/api/portraits/men/3.jpg", -"gender": "Male" -}, -{ -"first_name": "greg", -"last_name": "montgomery", -"image": "https://randomuser.me/api/portraits/men/12.jpg", -"gender": "Male" -}, -{ -"first_name": "matt", -"last_name": "lane", -"image": "https://randomuser.me/api/portraits/men/85.jpg", -"gender": "Male" -}, -{ -"first_name": "eleanor", -"last_name": "pearson", -"image": "https://randomuser.me/api/portraits/women/61.jpg", -"gender": "Female" -}, -{ -"first_name": "theodore", -"last_name": "burton", -"image": "https://randomuser.me/api/portraits/men/81.jpg", -"gender": "Male" -}, -{ -"first_name": "jesus", -"last_name": "hunt", -"image": "https://randomuser.me/api/portraits/men/50.jpg", -"gender": "Male" -}, -{ -"first_name": "taylor", -"last_name": "alvarez", -"image": "https://randomuser.me/api/portraits/men/0.jpg", -"gender": "Male" -}, -{ -"first_name": "barbara", -"last_name": "lucas", -"image": "https://randomuser.me/api/portraits/women/21.jpg", -"gender": "Female" -}, -{ -"first_name": "nicky", -"last_name": "simmons", -"image": "https://randomuser.me/api/portraits/women/29.jpg", -"gender": "Female" -}, -{ -"first_name": "arthur", -"last_name": "obrien", -"image": "https://randomuser.me/api/portraits/men/11.jpg", -"gender": "Male" -}, -{ -"first_name": "donna", -"last_name": "holmes", -"image": "https://randomuser.me/api/portraits/women/33.jpg", -"gender": "Female" -}, -{ -"first_name": "mitchell", -"last_name": "castro", -"image": "https://randomuser.me/api/portraits/men/26.jpg", -"gender": "Male" -}, -{ -"first_name": "byron", -"last_name": "marshall", -"image": "https://randomuser.me/api/portraits/men/57.jpg", -"gender": "Male" -}, -{ -"first_name": "larry", -"last_name": "king", -"image": "https://randomuser.me/api/portraits/men/58.jpg", -"gender": "Male" -}, -{ -"first_name": "deborah", -"last_name": "fuller", -"image": "https://randomuser.me/api/portraits/women/50.jpg", -"gender": "Female" -}, -{ -"first_name": "eleanor", -"last_name": "elliott", -"image": "https://randomuser.me/api/portraits/women/80.jpg", -"gender": "Female" -}, -{ -"first_name": "derrick", -"last_name": "shaw", -"image": "https://randomuser.me/api/portraits/men/78.jpg", -"gender": "Male" -}, -{ -"first_name": "barbara", -"last_name": "lynch", -"image": "https://randomuser.me/api/portraits/women/15.jpg", -"gender": "Female" -}, -{ -"first_name": "elijah", -"last_name": "allen", -"image": "https://randomuser.me/api/portraits/men/43.jpg", -"gender": "Male" -}, -{ -"first_name": "nicholas", -"last_name": "harper", -"image": "https://randomuser.me/api/portraits/men/2.jpg", -"gender": "Male" -}, -{ -"first_name": "sofia", -"last_name": "riley", -"image": "https://randomuser.me/api/portraits/women/96.jpg", -"gender": "Female" -}, -{ -"first_name": "jar", -"last_name": "hunt", -"image": "https://randomuser.me/api/portraits/men/72.jpg", -"gender": "Male" -}, -{ -"first_name": "philip", -"last_name": "rose", -"image": "https://randomuser.me/api/portraits/men/16.jpg", -"gender": "Male" -}, -{ -"first_name": "ella", -"last_name": "moore", -"image": "https://randomuser.me/api/portraits/women/83.jpg", -"gender": "Female" -}, -{ -"first_name": "seth", -"last_name": "tucker", -"image": "https://randomuser.me/api/portraits/men/6.jpg", -"gender": "Male" -}, -{ -"first_name": "abby", -"last_name": "gonzalez", -"image": "https://randomuser.me/api/portraits/women/18.jpg", -"gender": "Female" -}, -{ -"first_name": "noah", -"last_name": "williamson", -"image": "https://randomuser.me/api/portraits/men/54.jpg", -"gender": "Male" -}, -{ -"first_name": "cathy", -"last_name": "gray", -"image": "https://randomuser.me/api/portraits/women/88.jpg", -"gender": "Female" -}, -{ -"first_name": "barb", -"last_name": "snyder", -"image": "https://randomuser.me/api/portraits/women/49.jpg", -"gender": "Female" -}, -{ -"first_name": "rosalyn", -"last_name": "hale", -"image": "https://randomuser.me/api/portraits/women/64.jpg", -"gender": "Female" -}, -{ -"first_name": "jessica", -"last_name": "armstrong", -"image": "https://randomuser.me/api/portraits/women/95.jpg", -"gender": "Female" -}, -{ -"first_name": "vicki", -"last_name": "wheeler", -"image": "https://randomuser.me/api/portraits/women/49.jpg", -"gender": "Female" -}, -{ -"first_name": "luke", -"last_name": "fisher", -"image": "https://randomuser.me/api/portraits/men/77.jpg", -"gender": "Male" -}, -{ -"first_name": "joey", -"last_name": "wheeler", -"image": "https://randomuser.me/api/portraits/men/50.jpg", -"gender": "Male" -}, -{ -"first_name": "victoria", -"last_name": "jimenez", -"image": "https://randomuser.me/api/portraits/women/25.jpg", -"gender": "Female" -}, -{ -"first_name": "daryl", -"last_name": "patterson", -"image": "https://randomuser.me/api/portraits/men/30.jpg", -"gender": "Male" -}, -{ -"first_name": "dwayne", -"last_name": "jensen", -"image": "https://randomuser.me/api/portraits/men/71.jpg", -"gender": "Male" -}, -{ -"first_name": "herbert", -"last_name": "silva", -"image": "https://randomuser.me/api/portraits/men/83.jpg", -"gender": "Male" -}, -{ -"first_name": "walter", -"last_name": "walker", -"image": "https://randomuser.me/api/portraits/men/91.jpg", -"gender": "Male" -}, -{ -"first_name": "logan", -"last_name": "banks", -"image": "https://randomuser.me/api/portraits/men/67.jpg", -"gender": "Male" -}, -{ -"first_name": "shawn", -"last_name": "harvey", -"image": "https://randomuser.me/api/portraits/men/87.jpg", -"gender": "Male" -}, -{ -"first_name": "lawrence", -"last_name": "bradley", -"image": "https://randomuser.me/api/portraits/men/40.jpg", -"gender": "Male" -}, -{ -"first_name": "jack", -"last_name": "fleming", -"image": "https://randomuser.me/api/portraits/men/37.jpg", -"gender": "Male" -}, -{ -"first_name": "jackson", -"last_name": "boyd", -"image": "https://randomuser.me/api/portraits/men/68.jpg", -"gender": "Male" -}, -{ -"first_name": "cecil", -"last_name": "webb", -"image": "https://randomuser.me/api/portraits/men/9.jpg", -"gender": "Male" -}, -{ -"first_name": "eliza", -"last_name": "mills", -"image": "https://randomuser.me/api/portraits/women/20.jpg", -"gender": "Female" -}, -{ -"first_name": "jenny", -"last_name": "frazier", -"image": "https://randomuser.me/api/portraits/women/61.jpg", -"gender": "Female" -}, -{ -"first_name": "kent", -"last_name": "butler", -"image": "https://randomuser.me/api/portraits/men/64.jpg", -"gender": "Male" -}, -{ -"first_name": "rose", -"last_name": "perry", -"image": "https://randomuser.me/api/portraits/women/74.jpg", -"gender": "Female" -}, -{ -"first_name": "jack", -"last_name": "king", -"image": "https://randomuser.me/api/portraits/men/60.jpg", -"gender": "Male" -}, -{ -"first_name": "elmer", -"last_name": "williams", -"image": "https://randomuser.me/api/portraits/men/26.jpg", -"gender": "Male" -}, -{ -"first_name": "vanessa", -"last_name": "torres", -"image": "https://randomuser.me/api/portraits/women/41.jpg", -"gender": "Female" -}, -{ -"first_name": "tyrone", -"last_name": "coleman", -"image": "https://randomuser.me/api/portraits/men/59.jpg", -"gender": "Male" -}, -{ -"first_name": "julie", -"last_name": "bradley", -"image": "https://randomuser.me/api/portraits/women/50.jpg", -"gender": "Female" -}, -{ -"first_name": "fernando", -"last_name": "castro", -"image": "https://randomuser.me/api/portraits/men/44.jpg", -"gender": "Male" -}, -{ -"first_name": "sara", -"last_name": "craig", -"image": "https://randomuser.me/api/portraits/women/8.jpg", -"gender": "Female" -}, -{ -"first_name": "steven", -"last_name": "stone", -"image": "https://randomuser.me/api/portraits/men/47.jpg", -"gender": "Male" -}, -{ -"first_name": "barb", -"last_name": "rodriquez", -"image": "https://randomuser.me/api/portraits/women/73.jpg", -"gender": "Female" -}, -{ -"first_name": "charlie", -"last_name": "king", -"image": "https://randomuser.me/api/portraits/men/79.jpg", -"gender": "Male" -}, -{ -"first_name": "jessica", -"last_name": "davis", -"image": "https://randomuser.me/api/portraits/women/26.jpg", -"gender": "Female" -}, -{ -"first_name": "lewis", -"last_name": "watson", -"image": "https://randomuser.me/api/portraits/men/56.jpg", -"gender": "Male" -}, -{ -"first_name": "charlotte", -"last_name": "johnson", -"image": "https://randomuser.me/api/portraits/women/46.jpg", -"gender": "Female" -}, -{ -"first_name": "danielle", -"last_name": "bell", -"image": "https://randomuser.me/api/portraits/women/54.jpg", -"gender": "Female" -}, -{ -"first_name": "kristin", -"last_name": "dixon", -"image": "https://randomuser.me/api/portraits/women/23.jpg", -"gender": "Female" -}, -{ -"first_name": "andrea", -"last_name": "thompson", -"image": "https://randomuser.me/api/portraits/women/54.jpg", -"gender": "Female" -}, -{ -"first_name": "ashley", -"last_name": "andrews", -"image": "https://randomuser.me/api/portraits/women/46.jpg", -"gender": "Female" -}, -{ -"first_name": "sharon", -"last_name": "martinez", -"image": "https://randomuser.me/api/portraits/women/6.jpg", -"gender": "Female" -}, -{ -"first_name": "tristan", -"last_name": "cunningham", -"image": "https://randomuser.me/api/portraits/men/62.jpg", -"gender": "Male" -}, -{ -"first_name": "carol", -"last_name": "chavez", -"image": "https://randomuser.me/api/portraits/women/85.jpg", -"gender": "Female" -}, -{ -"first_name": "lauren", -"last_name": "hudson", -"image": "https://randomuser.me/api/portraits/women/88.jpg", -"gender": "Female" -}, -{ -"first_name": "guy", -"last_name": "robertson", -"image": "https://randomuser.me/api/portraits/men/78.jpg", -"gender": "Male" -}, -{ -"first_name": "debra", -"last_name": "long", -"image": "https://randomuser.me/api/portraits/women/23.jpg", -"gender": "Female" -}, -{ -"first_name": "taylor", -"last_name": "carpenter", -"image": "https://randomuser.me/api/portraits/men/0.jpg", -"gender": "Male" -}, -{ -"first_name": "eetu", -"last_name": "annala", -"image": "https://randomuser.me/api/portraits/men/31.jpg", -"gender": "Male" -}, -{ -"first_name": "oliver", -"last_name": "moilanen", -"image": "https://randomuser.me/api/portraits/men/14.jpg", -"gender": "Male" -}, -{ -"first_name": "leo", -"last_name": "maunu", -"image": "https://randomuser.me/api/portraits/men/72.jpg", -"gender": "Male" -}, -{ -"first_name": "iiris", -"last_name": "kalas", -"image": "https://randomuser.me/api/portraits/women/49.jpg", -"gender": "Female" -}, -{ -"first_name": "aada", -"last_name": "kinnunen", -"image": "https://randomuser.me/api/portraits/women/64.jpg", -"gender": "Female" -}, -{ -"first_name": "topias", -"last_name": "walli", -"image": "https://randomuser.me/api/portraits/men/58.jpg", -"gender": "Male" -}, -{ -"first_name": "viivi", -"last_name": "toivonen", -"image": "https://randomuser.me/api/portraits/women/16.jpg", -"gender": "Female" -}, -{ -"first_name": "iina", -"last_name": "makinen", -"image": "https://randomuser.me/api/portraits/women/44.jpg", -"gender": "Female" -}, -{ -"first_name": "lumi", -"last_name": "tuominen", -"image": "https://randomuser.me/api/portraits/women/11.jpg", -"gender": "Female" -}, -{ -"first_name": "ellen", -"last_name": "koski", -"image": "https://randomuser.me/api/portraits/women/22.jpg", -"gender": "Female" -}, -{ -"first_name": "onni", -"last_name": "laurila", -"image": "https://randomuser.me/api/portraits/men/74.jpg", -"gender": "Male" -}, -{ -"first_name": "eevi", -"last_name": "niskanen", -"image": "https://randomuser.me/api/portraits/women/72.jpg", -"gender": "Female" -}, -{ -"first_name": "julius", -"last_name": "maijala", -"image": "https://randomuser.me/api/portraits/men/8.jpg", -"gender": "Male" -}, -{ -"first_name": "sofia", -"last_name": "tuomi", -"image": "https://randomuser.me/api/portraits/women/1.jpg", -"gender": "Female" -}, -{ -"first_name": "oliver", -"last_name": "jarvela", -"image": "https://randomuser.me/api/portraits/men/60.jpg", -"gender": "Male" -}, -{ -"first_name": "luukas", -"last_name": "mikkola", -"image": "https://randomuser.me/api/portraits/men/90.jpg", -"gender": "Male" -}, -{ -"first_name": "amanda", -"last_name": "anttila", -"image": "https://randomuser.me/api/portraits/women/65.jpg", -"gender": "Female" -}, -{ -"first_name": "ella", -"last_name": "sakala", -"image": "https://randomuser.me/api/portraits/women/79.jpg", -"gender": "Female" -}, -{ -"first_name": "siiri", -"last_name": "kinnunen", -"image": "https://randomuser.me/api/portraits/women/37.jpg", -"gender": "Female" -}, -{ -"first_name": "joona", -"last_name": "korhonen", -"image": "https://randomuser.me/api/portraits/men/87.jpg", -"gender": "Male" -}, -{ -"first_name": "topias", -"last_name": "korpi", -"image": "https://randomuser.me/api/portraits/men/75.jpg", -"gender": "Male" -}, -{ -"first_name": "mikael", -"last_name": "remes", -"image": "https://randomuser.me/api/portraits/men/89.jpg", -"gender": "Male" -}, -{ -"first_name": "veera", -"last_name": "peltola", -"image": "https://randomuser.me/api/portraits/women/69.jpg", -"gender": "Female" -}, -{ -"first_name": "emil", -"last_name": "makela", -"image": "https://randomuser.me/api/portraits/men/98.jpg", -"gender": "Male" -}, -{ -"first_name": "luukas", -"last_name": "kujala", -"image": "https://randomuser.me/api/portraits/men/83.jpg", -"gender": "Male" -}, -{ -"first_name": "eemil", -"last_name": "honkala", -"image": "https://randomuser.me/api/portraits/men/85.jpg", -"gender": "Male" -}, -{ -"first_name": "peetu", -"last_name": "kalm", -"image": "https://randomuser.me/api/portraits/men/17.jpg", -"gender": "Male" -}, -{ -"first_name": "eemeli", -"last_name": "lehtonen", -"image": "https://randomuser.me/api/portraits/men/55.jpg", -"gender": "Male" -}, -{ -"first_name": "viivi", -"last_name": "koistinen", -"image": "https://randomuser.me/api/portraits/women/53.jpg", -"gender": "Female" -}, -{ -"first_name": "elli", -"last_name": "savela", -"image": "https://randomuser.me/api/portraits/women/77.jpg", -"gender": "Female" -}, -{ -"first_name": "venla", -"last_name": "walli", -"image": "https://randomuser.me/api/portraits/women/52.jpg", -"gender": "Female" -}, -{ -"first_name": "amanda", -"last_name": "wuollet", -"image": "https://randomuser.me/api/portraits/women/11.jpg", -"gender": "Female" -}, -{ -"first_name": "valtteri", -"last_name": "hokkanen", -"image": "https://randomuser.me/api/portraits/men/30.jpg", -"gender": "Male" -}, -{ -"first_name": "veera", -"last_name": "maki", -"image": "https://randomuser.me/api/portraits/women/34.jpg", -"gender": "Female" -}, -{ -"first_name": "kerttu", -"last_name": "maunu", -"image": "https://randomuser.me/api/portraits/women/1.jpg", -"gender": "Female" -}, -{ -"first_name": "nella", -"last_name": "hanka", -"image": "https://randomuser.me/api/portraits/women/70.jpg", -"gender": "Female" -}, -{ -"first_name": "iiris", -"last_name": "hakala", -"image": "https://randomuser.me/api/portraits/women/33.jpg", -"gender": "Female" -}, -{ -"first_name": "viivi", -"last_name": "ojala", -"image": "https://randomuser.me/api/portraits/women/69.jpg", -"gender": "Female" -}, -{ -"first_name": "iina", -"last_name": "peura", -"image": "https://randomuser.me/api/portraits/women/22.jpg", -"gender": "Female" -}, -{ -"first_name": "samuel", -"last_name": "mattila", -"image": "https://randomuser.me/api/portraits/men/88.jpg", -"gender": "Male" -}, -{ -"first_name": "julius", -"last_name": "kumpula", -"image": "https://randomuser.me/api/portraits/men/26.jpg", -"gender": "Male" -}, -{ -"first_name": "nooa", -"last_name": "haapala", -"image": "https://randomuser.me/api/portraits/men/77.jpg", -"gender": "Male" -}, -{ -"first_name": "elias", -"last_name": "leppo", -"image": "https://randomuser.me/api/portraits/men/50.jpg", -"gender": "Male" -}, -{ -"first_name": "niklas", -"last_name": "elo", -"image": "https://randomuser.me/api/portraits/men/64.jpg", -"gender": "Male" -}, -{ -"first_name": "olivia", -"last_name": "nurmi", -"image": "https://randomuser.me/api/portraits/women/82.jpg", -"gender": "Female" -}, -{ -"first_name": "milja", -"last_name": "lassila", -"image": "https://randomuser.me/api/portraits/women/47.jpg", -"gender": "Female" -}, -{ -"first_name": "daniel", -"last_name": "kalas", -"image": "https://randomuser.me/api/portraits/men/53.jpg", -"gender": "Male" -}, -{ -"first_name": "enni", -"last_name": "ramo", -"image": "https://randomuser.me/api/portraits/women/18.jpg", -"gender": "Female" -}, -{ -"first_name": "matilda", -"last_name": "salmi", -"image": "https://randomuser.me/api/portraits/women/84.jpg", -"gender": "Female" -}, -{ -"first_name": "valtteri", -"last_name": "wirta", -"image": "https://randomuser.me/api/portraits/men/26.jpg", -"gender": "Male" -}, -{ -"first_name": "julius", -"last_name": "maijala", -"image": "https://randomuser.me/api/portraits/men/39.jpg", -"gender": "Male" -}, -{ -"first_name": "kerttu", -"last_name": "peltola", -"image": "https://randomuser.me/api/portraits/women/39.jpg", -"gender": "Female" -}, -{ -"first_name": "aada", -"last_name": "kokko", -"image": "https://randomuser.me/api/portraits/women/26.jpg", -"gender": "Female" -}, -{ -"first_name": "elsa", -"last_name": "niska", -"image": "https://randomuser.me/api/portraits/women/26.jpg", -"gender": "Female" -}, -{ -"first_name": "ella", -"last_name": "kalm", -"image": "https://randomuser.me/api/portraits/women/61.jpg", -"gender": "Female" -}, -{ -"first_name": "lilja", -"last_name": "heinonen", -"image": "https://randomuser.me/api/portraits/women/65.jpg", -"gender": "Female" -}, -{ -"first_name": "akseli", -"last_name": "laakso", -"image": "https://randomuser.me/api/portraits/men/64.jpg", -"gender": "Male" -}, -{ -"first_name": "lotta", -"last_name": "saarela", -"image": "https://randomuser.me/api/portraits/women/69.jpg", -"gender": "Female" -}, -{ -"first_name": "leo", -"last_name": "polon", -"image": "https://randomuser.me/api/portraits/men/5.jpg", -"gender": "Male" -}, -{ -"first_name": "aleksi", -"last_name": "wuollet", -"image": "https://randomuser.me/api/portraits/men/87.jpg", -"gender": "Male" -}, -{ -"first_name": "eemil", -"last_name": "kalas", -"image": "https://randomuser.me/api/portraits/men/6.jpg", -"gender": "Male" -}, -{ -"first_name": "emmi", -"last_name": "koistinen", -"image": "https://randomuser.me/api/portraits/women/66.jpg", -"gender": "Female" -}, -{ -"first_name": "väinö", -"last_name": "halla", -"image": "https://randomuser.me/api/portraits/men/65.jpg", -"gender": "Male" -}, -{ -"first_name": "eemil", -"last_name": "heikkila", -"image": "https://randomuser.me/api/portraits/men/18.jpg", -"gender": "Male" -}, -{ -"first_name": "amanda", -"last_name": "lakso", -"image": "https://randomuser.me/api/portraits/women/29.jpg", -"gender": "Female" -}, -{ -"first_name": "vilho", -"last_name": "kivela", -"image": "https://randomuser.me/api/portraits/men/19.jpg", -"gender": "Male" -}, -{ -"first_name": "peppi", -"last_name": "lehtinen", -"image": "https://randomuser.me/api/portraits/women/80.jpg", -"gender": "Female" -}, -{ -"first_name": "onni", -"last_name": "lehtinen", -"image": "https://randomuser.me/api/portraits/men/0.jpg", -"gender": "Male" -}, -{ -"first_name": "onni", -"last_name": "ahonen", -"image": "https://randomuser.me/api/portraits/men/49.jpg", -"gender": "Male" -}, -{ -"first_name": "venla", -"last_name": "ranta", -"image": "https://randomuser.me/api/portraits/women/0.jpg", -"gender": "Female" -}, -{ -"first_name": "ronja", -"last_name": "korhonen", -"image": "https://randomuser.me/api/portraits/women/69.jpg", -"gender": "Female" -}, -{ -"first_name": "emmi", -"last_name": "niva", -"image": "https://randomuser.me/api/portraits/women/65.jpg", -"gender": "Female" -}, -{ -"first_name": "oskari", -"last_name": "leppanen", -"image": "https://randomuser.me/api/portraits/men/43.jpg", -"gender": "Male" -}, -{ -"first_name": "arttu", -"last_name": "heinonen", -"image": "https://randomuser.me/api/portraits/men/94.jpg", -"gender": "Male" -}, -{ -"first_name": "toivo", -"last_name": "makela", -"image": "https://randomuser.me/api/portraits/men/23.jpg", -"gender": "Male" -}, -{ -"first_name": "otto", -"last_name": "leino", -"image": "https://randomuser.me/api/portraits/men/51.jpg", -"gender": "Male" -}, -{ -"first_name": "milla", -"last_name": "kokko", -"image": "https://randomuser.me/api/portraits/women/66.jpg", -"gender": "Female" -}, -{ -"first_name": "konsta", -"last_name": "lehto", -"image": "https://randomuser.me/api/portraits/men/29.jpg", -"gender": "Male" -}, -{ -"first_name": "eeli", -"last_name": "heikkinen", -"image": "https://randomuser.me/api/portraits/men/50.jpg", -"gender": "Male" -}, -{ -"first_name": "matilda", -"last_name": "tanner", -"image": "https://randomuser.me/api/portraits/women/2.jpg", -"gender": "Female" -}, -{ -"first_name": "elias", -"last_name": "kivisto", -"image": "https://randomuser.me/api/portraits/men/40.jpg", -"gender": "Male" -}, -{ -"first_name": "akseli", -"last_name": "wirta", -"image": "https://randomuser.me/api/portraits/men/90.jpg", -"gender": "Male" -}, -{ -"first_name": "leevi", -"last_name": "kallio", -"image": "https://randomuser.me/api/portraits/men/89.jpg", -"gender": "Male" -}, -{ -"first_name": "emilia", -"last_name": "pelto", -"image": "https://randomuser.me/api/portraits/women/0.jpg", -"gender": "Female" -}, -{ -"first_name": "niilo", -"last_name": "keranen", -"image": "https://randomuser.me/api/portraits/men/29.jpg", -"gender": "Male" -}, -{ -"first_name": "mikael", -"last_name": "wainio", -"image": "https://randomuser.me/api/portraits/men/85.jpg", -"gender": "Male" -}, -{ -"first_name": "elias", -"last_name": "saksa", -"image": "https://randomuser.me/api/portraits/men/53.jpg", -"gender": "Male" -}, -{ -"first_name": "aatu", -"last_name": "erkkila", -"image": "https://randomuser.me/api/portraits/men/6.jpg", -"gender": "Male" -}, -{ -"first_name": "arttu", -"last_name": "jarvela", -"image": "https://randomuser.me/api/portraits/men/49.jpg", -"gender": "Male" -}, -{ -"first_name": "matilda", -"last_name": "lassila", -"image": "https://randomuser.me/api/portraits/women/46.jpg", -"gender": "Female" -}, -{ -"first_name": "alisa", -"last_name": "waara", -"image": "https://randomuser.me/api/portraits/women/67.jpg", -"gender": "Female" -}, -{ -"first_name": "emilia", -"last_name": "saksa", -"image": "https://randomuser.me/api/portraits/women/66.jpg", -"gender": "Female" -}, -{ -"first_name": "valtteri", -"last_name": "tikkanen", -"image": "https://randomuser.me/api/portraits/men/88.jpg", -"gender": "Male" -}, -{ -"first_name": "konsta", -"last_name": "rantala", -"image": "https://randomuser.me/api/portraits/men/50.jpg", -"gender": "Male" -}, -{ -"first_name": "minttu", -"last_name": "murto", -"image": "https://randomuser.me/api/portraits/women/14.jpg", -"gender": "Female" -}, -{ -"first_name": "vilma", -"last_name": "hatala", -"image": "https://randomuser.me/api/portraits/women/60.jpg", -"gender": "Female" -}, -{ -"first_name": "anni", -"last_name": "linna", -"image": "https://randomuser.me/api/portraits/women/59.jpg", -"gender": "Female" -}, -{ -"first_name": "niklas", -"last_name": "hautala", -"image": "https://randomuser.me/api/portraits/men/7.jpg", -"gender": "Male" -}, -{ -"first_name": "niilo", -"last_name": "lehtinen", -"image": "https://randomuser.me/api/portraits/men/54.jpg", -"gender": "Male" -}, -{ -"first_name": "oona", -"last_name": "saarinen", -"image": "https://randomuser.me/api/portraits/women/71.jpg", -"gender": "Female" -}, -{ -"first_name": "constance", -"last_name": "marie", -"image": "https://randomuser.me/api/portraits/women/40.jpg", -"gender": "Female" -}, -{ -"first_name": "charles", -"last_name": "pierre", -"image": "https://randomuser.me/api/portraits/men/96.jpg", -"gender": "Male" -}, -{ -"first_name": "bérénice", -"last_name": "leclerc", -"image": "https://randomuser.me/api/portraits/women/39.jpg", -"gender": "Female" -}, -{ -"first_name": "clémence", -"last_name": "arnaud", -"image": "https://randomuser.me/api/portraits/women/48.jpg", -"gender": "Female" -}, -{ -"first_name": "melvin", -"last_name": "lemoine", -"image": "https://randomuser.me/api/portraits/men/47.jpg", -"gender": "Male" -}, -{ -"first_name": "marceau", -"last_name": "joly", -"image": "https://randomuser.me/api/portraits/men/56.jpg", -"gender": "Male" -}, -{ -"first_name": "garance", -"last_name": "mathieu", -"image": "https://randomuser.me/api/portraits/women/87.jpg", -"gender": "Female" -}, -{ -"first_name": "angèle", -"last_name": "perrin", -"image": "https://randomuser.me/api/portraits/women/88.jpg", -"gender": "Female" -}, -{ -"first_name": "pauline", -"last_name": "simon", -"image": "https://randomuser.me/api/portraits/women/82.jpg", -"gender": "Female" -}, -{ -"first_name": "apolline", -"last_name": "laurent", -"image": "https://randomuser.me/api/portraits/women/27.jpg", -"gender": "Female" -}, -{ -"first_name": "luca", -"last_name": "lefevre", -"image": "https://randomuser.me/api/portraits/men/40.jpg", -"gender": "Male" -}, -{ -"first_name": "bastien", -"last_name": "roger", -"image": "https://randomuser.me/api/portraits/men/73.jpg", -"gender": "Male" -}, -{ -"first_name": "marie", -"last_name": "rodriguez", -"image": "https://randomuser.me/api/portraits/women/18.jpg", -"gender": "Female" -}, -{ -"first_name": "tristan", -"last_name": "renaud", -"image": "https://randomuser.me/api/portraits/men/41.jpg", -"gender": "Male" -}, -{ -"first_name": "eva", -"last_name": "philippe", -"image": "https://randomuser.me/api/portraits/women/26.jpg", -"gender": "Female" -}, -{ -"first_name": "coline", -"last_name": "dufour", -"image": "https://randomuser.me/api/portraits/women/64.jpg", -"gender": "Female" -}, -{ -"first_name": "marilou", -"last_name": "adam", -"image": "https://randomuser.me/api/portraits/women/53.jpg", -"gender": "Female" -}, -{ -"first_name": "lia", -"last_name": "renard", -"image": "https://randomuser.me/api/portraits/women/88.jpg", -"gender": "Female" -}, -{ -"first_name": "timothee", -"last_name": "rolland", -"image": "https://randomuser.me/api/portraits/men/75.jpg", -"gender": "Male" -}, -{ -"first_name": "hélèna", -"last_name": "boyer", -"image": "https://randomuser.me/api/portraits/women/8.jpg", -"gender": "Female" -}, -{ -"first_name": "mélody", -"last_name": "andre", -"image": "https://randomuser.me/api/portraits/women/75.jpg", -"gender": "Female" -}, -{ -"first_name": "jeanne", -"last_name": "duval", -"image": "https://randomuser.me/api/portraits/women/44.jpg", -"gender": "Female" -}, -{ -"first_name": "elias", -"last_name": "dupont", -"image": "https://randomuser.me/api/portraits/men/60.jpg", -"gender": "Male" -}, -{ -"first_name": "estelle", -"last_name": "bernard", -"image": "https://randomuser.me/api/portraits/women/23.jpg", -"gender": "Female" -}, -{ -"first_name": "roxane", -"last_name": "garnier", -"image": "https://randomuser.me/api/portraits/women/14.jpg", -"gender": "Female" -}, -{ -"first_name": "maëva", -"last_name": "guerin", -"image": "https://randomuser.me/api/portraits/women/44.jpg", -"gender": "Female" -}, -{ -"first_name": "liam", -"last_name": "carpentier", -"image": "https://randomuser.me/api/portraits/men/41.jpg", -"gender": "Male" -}, -{ -"first_name": "théo", -"last_name": "gaillard", -"image": "https://randomuser.me/api/portraits/men/40.jpg", -"gender": "Male" -}, -{ -"first_name": "angelina", -"last_name": "clement", -"image": "https://randomuser.me/api/portraits/women/53.jpg", -"gender": "Female" -}, -{ -"first_name": "emma", -"last_name": "bertrand", -"image": "https://randomuser.me/api/portraits/women/86.jpg", -"gender": "Female" -}, -{ -"first_name": "charles", -"last_name": "rolland", -"image": "https://randomuser.me/api/portraits/men/14.jpg", -"gender": "Male" -}, -{ -"first_name": "nolan", -"last_name": "gautier", -"image": "https://randomuser.me/api/portraits/men/6.jpg", -"gender": "Male" -}, -{ -"first_name": "agathe", -"last_name": "menard", -"image": "https://randomuser.me/api/portraits/women/69.jpg", -"gender": "Female" -}, -{ -"first_name": "gaëtan", -"last_name": "leclerc", -"image": "https://randomuser.me/api/portraits/men/60.jpg", -"gender": "Male" -}, -{ -"first_name": "clarisse", -"last_name": "lemaire", -"image": "https://randomuser.me/api/portraits/women/21.jpg", -"gender": "Female" -}, -{ -"first_name": "samuel", -"last_name": "garnier", -"image": "https://randomuser.me/api/portraits/men/16.jpg", -"gender": "Male" -}, -{ -"first_name": "eden", -"last_name": "fontai", -"image": "https://randomuser.me/api/portraits/women/17.jpg", -"gender": "Female" -}, -{ -"first_name": "maëva", -"last_name": "pierre", -"image": "https://randomuser.me/api/portraits/women/19.jpg", -"gender": "Female" -}, -{ -"first_name": "thomas", -"last_name": "barbier", -"image": "https://randomuser.me/api/portraits/men/31.jpg", -"gender": "Male" -}, -{ -"first_name": "lily", -"last_name": "lefebvre", -"image": "https://randomuser.me/api/portraits/women/76.jpg", -"gender": "Female" -}, -{ -"first_name": "lise", -"last_name": "perez", -"image": "https://randomuser.me/api/portraits/women/74.jpg", -"gender": "Female" -}, -{ -"first_name": "mila", -"last_name": "moulin", -"image": "https://randomuser.me/api/portraits/women/43.jpg", -"gender": "Female" -}, -{ -"first_name": "dylan", -"last_name": "picard", -"image": "https://randomuser.me/api/portraits/men/37.jpg", -"gender": "Male" -}, -{ -"first_name": "amandine", -"last_name": "rodriguez", -"image": "https://randomuser.me/api/portraits/women/65.jpg", -"gender": "Female" -}, -{ -"first_name": "diego", -"last_name": "girard", -"image": "https://randomuser.me/api/portraits/men/84.jpg", -"gender": "Male" -}, -{ -"first_name": "elouan", -"last_name": "garnier", -"image": "https://randomuser.me/api/portraits/men/94.jpg", -"gender": "Male" -}, -{ -"first_name": "apolline", -"last_name": "fleury", -"image": "https://randomuser.me/api/portraits/women/65.jpg", -"gender": "Female" -}, -{ -"first_name": "coline", -"last_name": "menard", -"image": "https://randomuser.me/api/portraits/women/83.jpg", -"gender": "Female" -}, -{ -"first_name": "maëly", -"last_name": "le gall", -"image": "https://randomuser.me/api/portraits/women/60.jpg", -"gender": "Female" -}, -{ -"first_name": "justin", -"last_name": "robert", -"image": "https://randomuser.me/api/portraits/men/20.jpg", -"gender": "Male" -}, -{ -"first_name": "ryan", -"last_name": "faure", -"image": "https://randomuser.me/api/portraits/men/16.jpg", -"gender": "Male" -}, -{ -"first_name": "ninon", -"last_name": "brunet", -"image": "https://randomuser.me/api/portraits/women/68.jpg", -"gender": "Female" -}, -{ -"first_name": "tessa", -"last_name": "garnier", -"image": "https://randomuser.me/api/portraits/women/54.jpg", -"gender": "Female" -}, -{ -"first_name": "ryan", -"last_name": "bonnet", -"image": "https://randomuser.me/api/portraits/men/28.jpg", -"gender": "Male" -}, -{ -"first_name": "aurélien", -"last_name": "andre", -"image": "https://randomuser.me/api/portraits/men/29.jpg", -"gender": "Male" -}, -{ -"first_name": "clément", -"last_name": "dumas", -"image": "https://randomuser.me/api/portraits/men/10.jpg", -"gender": "Male" -}, -{ -"first_name": "alexis", -"last_name": "fournier", -"image": "https://randomuser.me/api/portraits/men/83.jpg", -"gender": "Male" -}, -{ -"first_name": "valentin", -"last_name": "lecomte", -"image": "https://randomuser.me/api/portraits/men/44.jpg", -"gender": "Male" -}, -{ -"first_name": "florian", -"last_name": "olivier", -"image": "https://randomuser.me/api/portraits/men/36.jpg", -"gender": "Male" -}, -{ -"first_name": "ewen", -"last_name": "lefebvre", -"image": "https://randomuser.me/api/portraits/men/32.jpg", -"gender": "Male" -}, -{ -"first_name": "titouan", -"last_name": "charles", -"image": "https://randomuser.me/api/portraits/men/59.jpg", -"gender": "Male" -}, -{ -"first_name": "lila", -"last_name": "aubert", -"image": "https://randomuser.me/api/portraits/women/6.jpg", -"gender": "Female" -}, -{ -"first_name": "charline", -"last_name": "caron", -"image": "https://randomuser.me/api/portraits/women/49.jpg", -"gender": "Female" -}, -{ -"first_name": "soren", -"last_name": "le gall", -"image": "https://randomuser.me/api/portraits/men/77.jpg", -"gender": "Male" -}, -{ -"first_name": "fanny", -"last_name": "louis", -"image": "https://randomuser.me/api/portraits/women/90.jpg", -"gender": "Female" -}, -{ -"first_name": "julie", -"last_name": "adam", -"image": "https://randomuser.me/api/portraits/women/34.jpg", -"gender": "Female" -}, -{ -"first_name": "louka", -"last_name": "boyer", -"image": "https://randomuser.me/api/portraits/men/98.jpg", -"gender": "Male" -} -] diff --git a/erpnext/demo/data/room.json b/erpnext/demo/data/room.json deleted file mode 100644 index 82f0868b5a0..00000000000 --- a/erpnext/demo/data/room.json +++ /dev/null @@ -1,122 +0,0 @@ -[ - { - "doctype": "Room", - "room_name": "Lecture Hall 1", - "room_number": "101", - "seating_capacity": 80 - }, - { - "doctype": "Room", - "room_name": "Lecture Hall 2", - "room_number": "102", - "seating_capacity": 80 - }, - { - "doctype": "Room", - "room_name": "Lecture Hall 3", - "room_number": "103", - "seating_capacity": 80 - }, - { - "doctype": "Room", - "room_name": "Lecture Hall 4", - "room_number": "104", - "seating_capacity": 80 - }, - { - "doctype": "Room", - "room_name": "Lecture Hall 4", - "room_number": "104", - "seating_capacity": 80 - }, - { - "doctype": "Room", - "room_name": "Lecture Hall 5", - "room_number": "201", - "seating_capacity": 120 - }, - { - "doctype": "Room", - "room_name": "Lecture Hall 6", - "room_number": "202", - "seating_capacity": 120 - }, - { - "doctype": "Room", - "room_name": "Lecture Hall 7", - "room_number": "203", - "seating_capacity": 120 - }, - { - "doctype": "Room", - "room_name": "Computer Lab 1", - "room_number": "301", - "seating_capacity": 40 - }, - { - "doctype": "Room", - "room_name": "Computer Lab 2", - "room_number": "302", - "seating_capacity": 60 - }, - { - "doctype": "Room", - "room_name": "Seminar Hall 1", - "room_number": "303", - "seating_capacity": 240 - }, - { - "doctype": "Room", - "room_name": "Auditorium", - "room_number": "400", - "seating_capacity": 450 - }, - { - "doctype": "Room", - "room_name": "Exam hall 1", - "room_number": "560", - "seating_capacity": 70 - }, - { - "doctype": "Room", - "room_name": "Exam hall 2", - "room_number": "561", - "seating_capacity": 70 - }, - { - "doctype": "Room", - "room_name": "Exam hall 2", - "room_number": "562", - "seating_capacity": 70 - }, - { - "doctype": "Room", - "room_name": "Exam hall 3", - "room_number": "563", - "seating_capacity": 70 - }, - { - "doctype": "Room", - "room_name": "Exam hall 4", - "room_number": "564", - "seating_capacity": 70 - }, - { - "doctype": "Room", - "room_name": "Exam hall 5", - "room_number": "565", - "seating_capacity": 70 - }, - { - "doctype": "Room", - "room_name": "Exam hall 6", - "room_number": "566", - "seating_capacity": 70 - }, - { - "doctype": "Room", - "room_name": "Exam hall 7", - "room_number": "567", - "seating_capacity": 70 - } -] \ No newline at end of file diff --git a/erpnext/demo/data/student_batch_name.json b/erpnext/demo/data/student_batch_name.json deleted file mode 100644 index ef3f18dcf21..00000000000 --- a/erpnext/demo/data/student_batch_name.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "doctype": "Student Batch Name", - "batch_name": "Section-A" - }, - { - "doctype": "Student Batch Name", - "batch_name": "Section-B" - } -] \ No newline at end of file diff --git a/erpnext/demo/data/user.json b/erpnext/demo/data/user.json deleted file mode 100644 index 9ee5e780ac1..00000000000 --- a/erpnext/demo/data/user.json +++ /dev/null @@ -1,112 +0,0 @@ -[ - { - "email": "test_demo@erpnext.com", - "first_name": "Test", - "last_name": "User" - }, - { - "email": "DianaPrince@example.com", - "first_name": "Diana", - "last_name": "Prince" - }, - { - "email": "ZatannaZatara@example.com", - "first_name": "Zatanna", - "last_name": "Zatara" - }, - { - "email": "HollyGranger@example.com", - "first_name": "Holly", - "last_name": "Granger" - }, - { - "email": "NeptuniaAquaria@example.com", - "first_name": "Neptunia", - "last_name": "Aquaria" - }, - { - "email": "ArthurCurry@example.com", - "first_name": "Arthur", - "last_name": "Curry" - }, - { - "email": "ThaliaAlGhul@example.com", - "first_name": "Thalia", - "last_name": "Al Ghul" - }, - { - "email": "MaxwellLord@example.com", - "first_name": "Maxwell", - "last_name": "Lord" - }, - { - "email": "GraceChoi@example.com", - "first_name": "Grace", - "last_name": "Choi" - }, - { - "email": "VandalSavage@example.com", - "first_name": "Vandal", - "last_name": "Savage" - }, - { - "email": "CaitlinSnow@example.com", - "first_name": "Caitlin", - "last_name": "Snow" - }, - { - "email": "RipHunter@example.com", - "first_name": "Rip", - "last_name": "Hunter" - }, - { - "email": "NicholasFury@example.com", - "first_name": "Nicholas", - "last_name": "Fury" - }, - { - "email": "PeterParker@example.com", - "first_name": "Peter", - "last_name": "Parker" - }, - { - "email": "JohnConstantine@example.com", - "first_name": "John", - "last_name": "Constantine" - }, - { - "email": "HalJordan@example.com", - "first_name": "Hal", - "last_name": "Jordan" - }, - { - "email": "VictorStone@example.com", - "first_name": "Victor", - "last_name": "Stone" - }, - { - "email": "BruceWayne@example.com", - "first_name": "Bruce", - "last_name": "Wayne" - }, - { - "email": "ClarkKent@example.com", - "first_name": "Clark", - "last_name": "Kent" - }, - { - "email": "BarryAllen@example.com", - "first_name": "Barry", - "last_name": "Allen" - }, - { - "email": "KaraZorEl@example.com", - "first_name": "Kara", - "last_name": "Zor El" - }, - { - "email": "demo@erpnext.com", - "first_name": "Demo", - "last_name": "User" - } -] \ No newline at end of file diff --git a/erpnext/demo/demo.py b/erpnext/demo/demo.py deleted file mode 100644 index 4a18a99f41f..00000000000 --- a/erpnext/demo/demo.py +++ /dev/null @@ -1,97 +0,0 @@ -import sys - -import frappe -import frappe.utils - -import erpnext -from erpnext.demo.setup import education, manufacture, retail, setup_data -from erpnext.demo.user import accounts -from erpnext.demo.user import education as edu -from erpnext.demo.user import fixed_asset, hr, manufacturing, projects, purchase, sales, stock - -""" -Make a demo - -1. Start with a fresh account - -bench --site demo.erpnext.dev reinstall - -2. Install Demo - -bench --site demo.erpnext.dev execute erpnext.demo.demo.make - -3. If Demo breaks, to continue - -bench --site demo.erpnext.dev execute erpnext.demo.demo.simulate - -""" - -def make(domain='Manufacturing', days=100): - frappe.flags.domain = domain - frappe.flags.mute_emails = True - setup_data.setup(domain) - if domain== 'Manufacturing': - manufacture.setup_data() - elif domain == "Retail": - retail.setup_data() - elif domain== 'Education': - education.setup_data() - - site = frappe.local.site - frappe.destroy() - frappe.init(site) - frappe.connect() - - simulate(domain, days) - -def simulate(domain='Manufacturing', days=100): - runs_for = frappe.flags.runs_for or days - frappe.flags.company = erpnext.get_default_company() - frappe.flags.mute_emails = True - - if not frappe.flags.start_date: - # start date = 100 days back - frappe.flags.start_date = frappe.utils.add_days(frappe.utils.nowdate(), - -1 * runs_for) - - current_date = frappe.utils.getdate(frappe.flags.start_date) - - # continue? - demo_last_date = frappe.db.get_global('demo_last_date') - if demo_last_date: - current_date = frappe.utils.add_days(frappe.utils.getdate(demo_last_date), 1) - - # run till today - if not runs_for: - runs_for = frappe.utils.date_diff(frappe.utils.nowdate(), current_date) - # runs_for = 100 - - fixed_asset.work() - for i in range(runs_for): - sys.stdout.write("\rSimulating {0}: Day {1}".format( - current_date.strftime("%Y-%m-%d"), i)) - sys.stdout.flush() - frappe.flags.current_date = current_date - if current_date.weekday() in (5, 6): - current_date = frappe.utils.add_days(current_date, 1) - continue - try: - hr.work() - purchase.work() - stock.work() - accounts.work() - projects.run_projects(current_date) - sales.work(domain) - # run_messages() - - if domain=='Manufacturing': - manufacturing.work() - elif domain=='Education': - edu.work() - - except Exception: - frappe.db.set_global('demo_last_date', current_date) - raise - finally: - current_date = frappe.utils.add_days(current_date, 1) - frappe.db.commit() diff --git a/erpnext/demo/domains.py b/erpnext/demo/domains.py deleted file mode 100644 index 346787e3c7c..00000000000 --- a/erpnext/demo/domains.py +++ /dev/null @@ -1,20 +0,0 @@ -data = { - 'Manufacturing': { - 'company_name': 'Wind Power LLC' - }, - 'Retail': { - 'company_name': 'Mobile Next', - }, - 'Distribution': { - 'company_name': 'Soltice Hardware', - }, - 'Services': { - 'company_name': 'Acme Consulting' - }, - 'Education': { - 'company_name': 'Whitmore College' - }, - 'Non Profit': { - 'company_name': 'Erpnext Foundation' - } -} diff --git a/erpnext/demo/setup/education.py b/erpnext/demo/setup/education.py deleted file mode 100644 index eb833f4e0c0..00000000000 --- a/erpnext/demo/setup/education.py +++ /dev/null @@ -1,181 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import json -import random -from datetime import datetime - -import frappe -from frappe.utils.make_random import get_random - -from erpnext.demo.setup.setup_data import import_json - - -def setup_data(): - frappe.flags.mute_emails = True - make_masters() - setup_item() - make_student_applicants() - make_student_group() - make_fees_category() - make_fees_structure() - make_assessment_groups() - frappe.db.commit() - frappe.clear_cache() - -def make_masters(): - import_json("Room") - import_json("Department") - import_json("Instructor") - import_json("Course") - import_json("Program") - import_json("Student Batch Name") - import_json("Assessment Criteria") - import_json("Grading Scale") - frappe.db.commit() - -def setup_item(): - items = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'item_education.json')).read()) - for i in items: - item = frappe.new_doc('Item') - item.update(i) - item.min_order_qty = random.randint(10, 30) - item.item_defaults[0].default_warehouse = frappe.get_all('Warehouse', - filters={'warehouse_name': item.item_defaults[0].default_warehouse}, limit=1)[0].name - item.insert() - -def make_student_applicants(): - blood_group = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"] - male_names = [] - female_names = [] - - file_path = get_json_path("Random Student Data") - with open(file_path, "r") as open_file: - random_student_data = json.loads(open_file.read()) - count = 1 - - for d in random_student_data: - if d.get('gender') == "Male": - male_names.append(d.get('first_name').title()) - - if d.get('gender') == "Female": - female_names.append(d.get('first_name').title()) - - for idx, d in enumerate(random_student_data): - student_applicant = frappe.new_doc("Student Applicant") - student_applicant.first_name = d.get('first_name').title() - student_applicant.last_name = d.get('last_name').title() - student_applicant.image = d.get('image') - student_applicant.gender = d.get('gender') - student_applicant.program = get_random("Program") - student_applicant.blood_group = random.choice(blood_group) - year = random.randint(1990, 1998) - month = random.randint(1, 12) - day = random.randint(1, 28) - student_applicant.date_of_birth = datetime(year, month, day) - student_applicant.mother_name = random.choice(female_names) + " " + d.get('last_name').title() - student_applicant.father_name = random.choice(male_names) + " " + d.get('last_name').title() - if student_applicant.gender == "Male": - student_applicant.middle_name = random.choice(male_names) - else: - student_applicant.middle_name = random.choice(female_names) - student_applicant.student_email_id = d.get('first_name') + "_" + \ - student_applicant.middle_name + "_" + d.get('last_name') + "@example.com" - if count <5: - student_applicant.insert() - frappe.db.commit() - else: - student_applicant.submit() - frappe.db.commit() - count+=1 - -def make_student_group(): - for term in frappe.db.get_list("Academic Term"): - for program in frappe.db.get_list("Program"): - sg_tool = frappe.new_doc("Student Group Creation Tool") - sg_tool.academic_year = "2017-18" - sg_tool.academic_term = term.name - sg_tool.program = program.name - for d in sg_tool.get_courses(): - d = frappe._dict(d) - student_group = frappe.new_doc("Student Group") - student_group.student_group_name = d.student_group_name - student_group.group_based_on = d.group_based_on - student_group.program = program.name - student_group.course = d.course - student_group.batch = d.batch - student_group.academic_term = term.name - student_group.academic_year = "2017-18" - student_group.save() - frappe.db.commit() - -def make_fees_category(): - fee_type = ["Tuition Fee", "Hostel Fee", "Logistics Fee", - "Medical Fee", "Mess Fee", "Security Deposit"] - - fee_desc = {"Tuition Fee" : "Curricular activities which includes books, notebooks and faculty charges" , - "Hostel Fee" : "Stay of students in institute premises", - "Logistics Fee" : "Lodging boarding of the students" , - "Medical Fee" : "Medical welfare of the students", - "Mess Fee" : "Food and beverages for your ward", - "Security Deposit" : "In case your child is found to have damaged institutes property" - } - - for i in fee_type: - fee_category = frappe.new_doc("Fee Category") - fee_category.category_name = i - fee_category.description = fee_desc[i] - fee_category.insert() - frappe.db.commit() - -def make_fees_structure(): - for d in frappe.db.get_list("Program"): - program = frappe.get_doc("Program", d.name) - for academic_term in ["2017-18 (Semester 1)", "2017-18 (Semester 2)", "2017-18 (Semester 3)"]: - fee_structure = frappe.new_doc("Fee Structure") - fee_structure.program = d.name - fee_structure.academic_term = random.choice(frappe.db.get_list("Academic Term")).name - for j in range(1,4): - temp = {"fees_category": random.choice(frappe.db.get_list("Fee Category")).name , "amount" : random.randint(500,1000)} - fee_structure.append("components", temp) - fee_structure.insert() - program.append("fees", {"academic_term": academic_term, "fee_structure": fee_structure.name, "amount": fee_structure.total_amount}) - program.save() - frappe.db.commit() - -def make_assessment_groups(): - for year in frappe.db.get_list("Academic Year"): - ag = frappe.new_doc('Assessment Group') - ag.assessment_group_name = year.name - ag.parent_assessment_group = "All Assessment Groups" - ag.is_group = 1 - ag.insert() - for term in frappe.db.get_list("Academic Term", filters = {"academic_year": year.name}): - ag1 = frappe.new_doc('Assessment Group') - ag1.assessment_group_name = term.name - ag1.parent_assessment_group = ag.name - ag1.is_group = 1 - ag1.insert() - for assessment_group in ['Term I', 'Term II']: - ag2 = frappe.new_doc('Assessment Group') - ag2.assessment_group_name = ag1.name + " " + assessment_group - ag2.parent_assessment_group = ag1.name - ag2.insert() - frappe.db.commit() - - -def get_json_path(doctype): - return frappe.get_app_path('erpnext', 'demo', 'data', frappe.scrub(doctype) + '.json') - -def weighted_choice(weights): - totals = [] - running_total = 0 - - for w in weights: - running_total += w - totals.append(running_total) - - rnd = random.random() * running_total - for i, total in enumerate(totals): - if rnd < total: - return i diff --git a/erpnext/demo/setup/manufacture.py b/erpnext/demo/setup/manufacture.py deleted file mode 100644 index fe1a1fb2034..00000000000 --- a/erpnext/demo/setup/manufacture.py +++ /dev/null @@ -1,140 +0,0 @@ -import json -import random - -import frappe -from frappe.utils import add_days, nowdate - -from erpnext.demo.domains import data -from erpnext.demo.setup.setup_data import import_json - - -def setup_data(): - import_json("Location") - import_json("Asset Category") - setup_item() - setup_workstation() - setup_asset() - import_json('Operation') - setup_item_price() - show_item_groups_in_website() - import_json('BOM', submit=True) - frappe.db.commit() - frappe.clear_cache() - -def setup_workstation(): - workstations = [u'Drilling Machine 1', u'Lathe 1', u'Assembly Station 1', u'Assembly Station 2', u'Packing and Testing Station'] - for w in workstations: - frappe.get_doc({ - "doctype": "Workstation", - "workstation_name": w, - "holiday_list": frappe.get_all("Holiday List")[0].name, - "hour_rate_consumable": int(random.random() * 20), - "hour_rate_electricity": int(random.random() * 10), - "hour_rate_labour": int(random.random() * 40), - "hour_rate_rent": int(random.random() * 10), - "working_hours": [ - { - "enabled": 1, - "start_time": "8:00:00", - "end_time": "15:00:00" - } - ] - }).insert() - -def show_item_groups_in_website(): - """set show_in_website=1 for Item Groups""" - products = frappe.get_doc("Item Group", "Products") - products.show_in_website = 1 - products.route = 'products' - products.save() - -def setup_asset(): - assets = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'asset.json')).read()) - for d in assets: - asset = frappe.new_doc('Asset') - asset.update(d) - asset.purchase_date = add_days(nowdate(), -random.randint(20, 1500)) - asset.next_depreciation_date = add_days(asset.purchase_date, 30) - asset.warehouse = "Stores - WPL" - asset.set_missing_values() - asset.make_depreciation_schedule() - asset.flags.ignore_validate = True - asset.flags.ignore_mandatory = True - asset.save() - asset.submit() - -def setup_item(): - items = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'item.json')).read()) - for i in items: - item = frappe.new_doc('Item') - item.update(i) - if hasattr(item, 'item_defaults') and item.item_defaults[0].default_warehouse: - item.item_defaults[0].company = data.get("Manufacturing").get('company_name') - warehouse = frappe.get_all('Warehouse', filters={'warehouse_name': item.item_defaults[0].default_warehouse}, limit=1) - if warehouse: - item.item_defaults[0].default_warehouse = warehouse[0].name - item.insert() - -def setup_product_bundle(): - frappe.get_doc({ - 'doctype': 'Product Bundle', - 'new_item_code': 'Wind Mill A Series with Spare Bearing', - 'items': [ - {'item_code': 'Wind Mill A Series', 'qty': 1}, - {'item_code': 'Bearing Collar', 'qty': 1}, - {'item_code': 'Bearing Assembly', 'qty': 1}, - ] - }).insert() - -def setup_item_price(): - frappe.db.sql("delete from `tabItem Price`") - - standard_selling = { - "Base Bearing Plate": 28, - "Base Plate": 21, - "Bearing Assembly": 300, - "Bearing Block": 14, - "Bearing Collar": 103.6, - "Bearing Pipe": 63, - "Blade Rib": 46.2, - "Disc Collars": 42, - "External Disc": 56, - "Internal Disc": 70, - "Shaft": 340, - "Stand": 400, - "Upper Bearing Plate": 300, - "Wind Mill A Series": 320, - "Wind Mill A Series with Spare Bearing": 750, - "Wind MIll C Series": 400, - "Wind Turbine": 400, - "Wing Sheet": 30.8 - } - - standard_buying = { - "Base Bearing Plate": 20, - "Base Plate": 28, - "Base Plate Un Painted": 16, - "Bearing Block": 13, - "Bearing Collar": 96.4, - "Bearing Pipe": 55, - "Blade Rib": 38, - "Disc Collars": 34, - "External Disc": 50, - "Internal Disc": 60, - "Shaft": 250, - "Stand": 300, - "Upper Bearing Plate": 200, - "Wing Sheet": 25 - } - - for price_list in ("standard_buying", "standard_selling"): - for item, rate in locals().get(price_list).items(): - frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list.replace("_", " ").title(), - "item_code": item, - "selling": 1 if price_list=="standard_selling" else 0, - "buying": 1 if price_list=="standard_buying" else 0, - "price_list_rate": rate, - "currency": "USD" - }).insert() diff --git a/erpnext/demo/setup/retail.py b/erpnext/demo/setup/retail.py deleted file mode 100644 index 0469264da1e..00000000000 --- a/erpnext/demo/setup/retail.py +++ /dev/null @@ -1,62 +0,0 @@ -import json - -import frappe - -from erpnext.demo.domains import data - - -def setup_data(): - setup_item() - setup_item_price() - frappe.db.commit() - frappe.clear_cache() - -def setup_item(): - items = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'item.json')).read()) - for i in items: - if not i.get("domain") == "Retail": continue - item = frappe.new_doc('Item') - item.update(i) - if hasattr(item, 'item_defaults') and item.item_defaults[0].default_warehouse: - item.item_defaults[0].company = data.get("Retail").get('company_name') - warehouse = frappe.get_all('Warehouse', filters={'warehouse_name': item.item_defaults[0].default_warehouse}, limit=1) - if warehouse: - item.item_defaults[0].default_warehouse = warehouse[0].name - item.insert() - -def setup_item_price(): - frappe.db.sql("delete from `tabItem Price`") - - standard_selling = { - "OnePlus 6": 579, - "OnePlus 6T": 600, - "Xiaomi Poco F1": 300, - "Iphone XS": 999, - "Samsung Galaxy S9": 720, - "Sony Bluetooth Headphone": 99, - "Xiaomi Phone Repair": 10, - "Samsung Phone Repair": 20, - "OnePlus Phone Repair": 15, - "Apple Phone Repair": 30, - } - - standard_buying = { - "OnePlus 6": 300, - "OnePlus 6T": 350, - "Xiaomi Poco F1": 200, - "Iphone XS": 600, - "Samsung Galaxy S9": 500, - "Sony Bluetooth Headphone": 69 - } - - for price_list in ("standard_buying", "standard_selling"): - for item, rate in locals().get(price_list).items(): - frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list.replace("_", " ").title(), - "item_code": item, - "selling": 1 if price_list=="standard_selling" else 0, - "buying": 1 if price_list=="standard_buying" else 0, - "price_list_rate": rate, - "currency": "USD" - }).insert() diff --git a/erpnext/demo/setup/setup_data.py b/erpnext/demo/setup/setup_data.py deleted file mode 100644 index 7137c6ef564..00000000000 --- a/erpnext/demo/setup/setup_data.py +++ /dev/null @@ -1,447 +0,0 @@ -import json -import random - -import frappe -from frappe import _ -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields -from frappe.utils import cstr, flt, now_datetime, random_string -from frappe.utils.make_random import add_random_children, get_random -from frappe.utils.nestedset import get_root_of - -import erpnext -from erpnext.demo.domains import data - - -def setup(domain): - frappe.flags.in_demo = 1 - complete_setup(domain) - setup_demo_page() - setup_fiscal_year() - setup_holiday_list() - setup_user() - setup_employee() - setup_user_roles(domain) - setup_role_permissions() - setup_custom_field_for_domain() - - employees = frappe.get_all('Employee', fields=['name', 'date_of_joining']) - - # monthly salary - setup_salary_structure(employees[:5], 0) - - # based on timesheet - setup_salary_structure(employees[5:], 1) - - setup_leave_allocation() - setup_customer() - setup_supplier() - setup_warehouse() - import_json('Address') - import_json('Contact') - import_json('Lead') - setup_currency_exchange() - #setup_mode_of_payment() - setup_account_to_expense_type() - setup_budget() - setup_pos_profile() - - frappe.db.commit() - frappe.clear_cache() - -def complete_setup(domain='Manufacturing'): - print("Complete Setup...") - from frappe.desk.page.setup_wizard.setup_wizard import setup_complete - - if not frappe.get_all('Company', limit=1): - setup_complete({ - "full_name": "Test User", - "email": "test_demo@erpnext.com", - "company_tagline": 'Awesome Products and Services', - "password": "demo", - "fy_start_date": "2015-01-01", - "fy_end_date": "2015-12-31", - "bank_account": "National Bank", - "domains": [domain], - "company_name": data.get(domain).get('company_name'), - "chart_of_accounts": "Standard", - "company_abbr": ''.join([d[0] for d in data.get(domain).get('company_name').split()]).upper(), - "currency": 'USD', - "timezone": 'America/New_York', - "country": 'United States', - "language": "english" - }) - - company = erpnext.get_default_company() - - if company: - company_doc = frappe.get_doc("Company", company) - company_doc.db_set('default_payroll_payable_account', - frappe.db.get_value('Account', dict(account_name='Payroll Payable'))) - -def setup_demo_page(): - # home page should always be "start" - website_settings = frappe.get_doc("Website Settings", "Website Settings") - website_settings.home_page = "demo" - website_settings.save() - -def setup_fiscal_year(): - fiscal_year = None - for year in range(2010, now_datetime().year + 1, 1): - try: - fiscal_year = frappe.get_doc({ - "doctype": "Fiscal Year", - "year": cstr(year), - "year_start_date": "{0}-01-01".format(year), - "year_end_date": "{0}-12-31".format(year) - }).insert() - except frappe.DuplicateEntryError: - pass - - # set the last fiscal year (current year) as default - if fiscal_year: - fiscal_year.set_as_default() - -def setup_holiday_list(): - """Setup Holiday List for the current year""" - year = now_datetime().year - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": str(year), - "from_date": "{0}-01-01".format(year), - "to_date": "{0}-12-31".format(year), - }) - holiday_list.insert() - holiday_list.weekly_off = "Saturday" - holiday_list.get_weekly_off_dates() - holiday_list.weekly_off = "Sunday" - holiday_list.get_weekly_off_dates() - holiday_list.save() - - frappe.set_value("Company", erpnext.get_default_company(), "default_holiday_list", holiday_list.name) - - -def setup_user(): - frappe.db.sql('delete from tabUser where name not in ("Guest", "Administrator")') - for u in json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'user.json')).read()): - user = frappe.new_doc("User") - user.update(u) - user.flags.no_welcome_mail = True - user.new_password = 'Demo1234567!!!' - user.insert() - -def setup_employee(): - frappe.db.set_value("HR Settings", None, "emp_created_by", "Naming Series") - frappe.db.commit() - - for d in frappe.get_all('Salary Component'): - salary_component = frappe.get_doc('Salary Component', d.name) - salary_component.append('accounts', dict( - company=erpnext.get_default_company(), - account=frappe.get_value('Account', dict(account_name=('like', 'Salary%'))) - )) - salary_component.save() - - import_json('Employee') - holiday_list = frappe.db.get_value("Holiday List", {"holiday_list_name": str(now_datetime().year)}, 'name') - frappe.db.sql('''update tabEmployee set holiday_list={0}'''.format(holiday_list)) - -def setup_salary_structure(employees, salary_slip_based_on_timesheet=0): - ss = frappe.new_doc('Salary Structure') - ss.name = "Sample Salary Structure - " + random_string(5) - ss.salary_slip_based_on_timesheet = salary_slip_based_on_timesheet - - if salary_slip_based_on_timesheet: - ss.salary_component = 'Basic' - ss.hour_rate = flt(random.random() * 10, 2) - else: - ss.payroll_frequency = 'Monthly' - - ss.payment_account = frappe.get_value('Account', - {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name") - - ss.append('earnings', { - 'salary_component': 'Basic', - "abbr":'B', - 'formula': 'base*.2', - 'amount_based_on_formula': 1, - "idx": 1 - }) - ss.append('deductions', { - 'salary_component': 'Income Tax', - "abbr":'IT', - 'condition': 'base > 10000', - 'formula': 'base*.1', - "idx": 1 - }) - ss.insert() - ss.submit() - - for e in employees: - sa = frappe.new_doc("Salary Structure Assignment") - sa.employee = e.name - sa.salary_structure = ss.name - sa.from_date = "2015-01-01" - sa.base = random.random() * 10000 - sa.insert() - sa.submit() - - return ss - -def setup_user_roles(domain): - user = frappe.get_doc('User', 'demo@erpnext.com') - user.add_roles('HR User', 'HR Manager', 'Accounts User', 'Accounts Manager', - 'Stock User', 'Stock Manager', 'Sales User', 'Sales Manager', 'Purchase User', - 'Purchase Manager', 'Projects User', 'Manufacturing User', 'Manufacturing Manager', - 'Support Team') - - if domain == "Education": - user.add_roles('Academics User') - - if not frappe.db.get_global('demo_hr_user'): - user = frappe.get_doc('User', 'CaitlinSnow@example.com') - user.add_roles('HR User', 'HR Manager', 'Accounts User') - frappe.db.set_global('demo_hr_user', user.name) - update_employee_department(user.name, 'Human Resources') - for d in frappe.get_all('User Permission', filters={"user": "CaitlinSnow@example.com"}): - frappe.delete_doc('User Permission', d.name) - - if not frappe.db.get_global('demo_sales_user_1'): - user = frappe.get_doc('User', 'VandalSavage@example.com') - user.add_roles('Sales User') - update_employee_department(user.name, 'Sales') - frappe.db.set_global('demo_sales_user_1', user.name) - - if not frappe.db.get_global('demo_sales_user_2'): - user = frappe.get_doc('User', 'GraceChoi@example.com') - user.add_roles('Sales User', 'Sales Manager', 'Accounts User') - update_employee_department(user.name, 'Sales') - frappe.db.set_global('demo_sales_user_2', user.name) - - if not frappe.db.get_global('demo_purchase_user'): - user = frappe.get_doc('User', 'MaxwellLord@example.com') - user.add_roles('Purchase User', 'Purchase Manager', 'Accounts User', 'Stock User') - update_employee_department(user.name, 'Purchase') - frappe.db.set_global('demo_purchase_user', user.name) - - if not frappe.db.get_global('demo_manufacturing_user'): - user = frappe.get_doc('User', 'NeptuniaAquaria@example.com') - user.add_roles('Manufacturing User', 'Stock Manager', 'Stock User', 'Purchase User', 'Accounts User') - update_employee_department(user.name, 'Production') - frappe.db.set_global('demo_manufacturing_user', user.name) - - if not frappe.db.get_global('demo_stock_user'): - user = frappe.get_doc('User', 'HollyGranger@example.com') - user.add_roles('Manufacturing User', 'Stock User', 'Purchase User', 'Accounts User') - update_employee_department(user.name, 'Production') - frappe.db.set_global('demo_stock_user', user.name) - - if not frappe.db.get_global('demo_accounts_user'): - user = frappe.get_doc('User', 'BarryAllen@example.com') - user.add_roles('Accounts User', 'Accounts Manager', 'Sales User', 'Purchase User') - update_employee_department(user.name, 'Accounts') - frappe.db.set_global('demo_accounts_user', user.name) - - if not frappe.db.get_global('demo_projects_user'): - user = frappe.get_doc('User', 'PeterParker@example.com') - user.add_roles('HR User', 'Projects User') - update_employee_department(user.name, 'Management') - frappe.db.set_global('demo_projects_user', user.name) - - if domain == "Education": - if not frappe.db.get_global('demo_education_user'): - user = frappe.get_doc('User', 'ArthurCurry@example.com') - user.add_roles('Academics User') - update_employee_department(user.name, 'Management') - frappe.db.set_global('demo_education_user', user.name) - - #Add Expense Approver - user = frappe.get_doc('User', 'ClarkKent@example.com') - user.add_roles('Expense Approver') - -def setup_leave_allocation(): - year = now_datetime().year - for employee in frappe.get_all('Employee', fields=['name']): - leave_types = frappe.get_all("Leave Type", fields=['name', 'max_continuous_days_allowed']) - for leave_type in leave_types: - if not leave_type.max_continuous_days_allowed: - leave_type.max_continuous_days_allowed = 10 - - leave_allocation = frappe.get_doc({ - "doctype": "Leave Allocation", - "employee": employee.name, - "from_date": "{0}-01-01".format(year), - "to_date": "{0}-12-31".format(year), - "leave_type": leave_type.name, - "new_leaves_allocated": random.randint(1, int(leave_type.max_continuous_days_allowed)) - }) - leave_allocation.insert() - leave_allocation.submit() - frappe.db.commit() - -def setup_customer(): - customers = [u'Asian Junction', u'Life Plan Counselling', u'Two Pesos', u'Mr Fables', u'Intelacard', u'Big D Supermarkets', u'Adaptas', u'Nelson Brothers', u'Landskip Yard Care', u'Buttrey Food & Drug', u'Fayva', u'Asian Fusion', u'Crafts Canada', u'Consumers and Consumers Express', u'Netobill', u'Choices', u'Chi-Chis', u'Red Food', u'Endicott Shoes', u'Hind Enterprises'] - for c in customers: - frappe.get_doc({ - "doctype": "Customer", - "customer_name": c, - "customer_group": "Commercial", - "customer_type": random.choice(["Company", "Individual"]), - "territory": "Rest Of The World" - }).insert() - -def setup_supplier(): - suppliers = [u'Helios Air', u'Ks Merchandise', u'HomeBase', u'Scott Ties', u'Reliable Investments', u'Nan Duskin', u'Rainbow Records', u'New World Realty', u'Asiatic Solutions', u'Eagle Hardware', u'Modern Electricals'] - for s in suppliers: - frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": s, - "supplier_group": random.choice(["Services", "Raw Material"]), - }).insert() - -def setup_warehouse(): - w = frappe.new_doc('Warehouse') - w.warehouse_name = 'Supplier' - w.insert() - -def setup_currency_exchange(): - frappe.get_doc({ - 'doctype': 'Currency Exchange', - 'from_currency': 'EUR', - 'to_currency': 'USD', - 'exchange_rate': 1.13 - }).insert() - - frappe.get_doc({ - 'doctype': 'Currency Exchange', - 'from_currency': 'CNY', - 'to_currency': 'USD', - 'exchange_rate': 0.16 - }).insert() - -def setup_mode_of_payment(): - company_abbr = frappe.get_cached_value('Company', erpnext.get_default_company(), "abbr") - account_dict = {'Cash': 'Cash - '+ company_abbr , 'Bank': 'National Bank - '+ company_abbr} - for payment_mode in frappe.get_all('Mode of Payment', fields = ["name", "type"]): - if payment_mode.type: - mop = frappe.get_doc('Mode of Payment', payment_mode.name) - mop.append('accounts', { - 'company': erpnext.get_default_company(), - 'default_account': account_dict.get(payment_mode.type) - }) - mop.save(ignore_permissions=True) - -def setup_account(): - frappe.flags.in_import = True - data = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', - 'account.json')).read()) - for d in data: - doc = frappe.new_doc('Account') - doc.update(d) - doc.parent_account = frappe.db.get_value('Account', {'account_name': doc.parent_account}) - doc.insert() - - frappe.flags.in_import = False - -def setup_account_to_expense_type(): - company_abbr = frappe.get_cached_value('Company', erpnext.get_default_company(), "abbr") - expense_types = [{'name': _('Calls'), "account": "Sales Expenses - "+ company_abbr}, - {'name': _('Food'), "account": "Entertainment Expenses - "+ company_abbr}, - {'name': _('Medical'), "account": "Utility Expenses - "+ company_abbr}, - {'name': _('Others'), "account": "Miscellaneous Expenses - "+ company_abbr}, - {'name': _('Travel'), "account": "Travel Expenses - "+ company_abbr}] - - for expense_type in expense_types: - doc = frappe.get_doc("Expense Claim Type", expense_type["name"]) - doc.append("accounts", { - "company" : erpnext.get_default_company(), - "default_account" : expense_type["account"] - }) - doc.save(ignore_permissions=True) - -def setup_budget(): - fiscal_years = frappe.get_all("Fiscal Year", order_by="year_start_date")[-2:] - - for fy in fiscal_years: - budget = frappe.new_doc("Budget") - budget.cost_center = get_random("Cost Center") - budget.fiscal_year = fy.name - budget.action_if_annual_budget_exceeded = "Warn" - expense_ledger_count = frappe.db.count("Account", {"is_group": "0", "root_type": "Expense"}) - - add_random_children(budget, "accounts", rows=random.randint(10, expense_ledger_count), - randomize = { - "account": ("Account", {"is_group": "0", "root_type": "Expense"}) - }, unique="account") - - for d in budget.accounts: - d.budget_amount = random.randint(5, 100) * 10000 - - budget.save() - budget.submit() - -def setup_pos_profile(): - company_abbr = frappe.get_cached_value('Company', erpnext.get_default_company(), "abbr") - pos = frappe.new_doc('POS Profile') - pos.user = frappe.db.get_global('demo_accounts_user') - pos.name = "Demo POS Profile" - pos.naming_series = 'SINV-' - pos.update_stock = 0 - pos.write_off_account = 'Cost of Goods Sold - '+ company_abbr - pos.write_off_cost_center = 'Main - '+ company_abbr - pos.customer_group = get_root_of('Customer Group') - pos.territory = get_root_of('Territory') - - pos.append('payments', { - 'mode_of_payment': frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), - 'amount': 0.0, - 'default': 1 - }) - - pos.insert() - -def setup_role_permissions(): - role_permissions = {'Batch': ['Accounts User', 'Item Manager']} - for doctype, roles in role_permissions.items(): - for role in roles: - if not frappe.db.get_value('Custom DocPerm', - {'parent': doctype, 'role': role}): - frappe.get_doc({ - 'doctype': 'Custom DocPerm', - 'role': role, - 'read': 1, - 'write': 1, - 'create': 1, - 'delete': 1, - 'parent': doctype - }).insert(ignore_permissions=True) - -def import_json(doctype, submit=False, values=None): - frappe.flags.in_import = True - data = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', - frappe.scrub(doctype) + '.json')).read()) - for d in data: - doc = frappe.new_doc(doctype) - doc.update(d) - doc.insert() - if submit: - doc.submit() - - frappe.db.commit() - - frappe.flags.in_import = False - -def update_employee_department(user_id, department): - employee = frappe.db.get_value('Employee', {"user_id": user_id}, 'name') - department = frappe.db.get_value('Department', {'department_name': department}, 'name') - frappe.db.set_value('Employee', employee, 'department', department) - -def setup_custom_field_for_domain(): - field = { - "Item": [ - dict(fieldname='domain', label='Domain', - fieldtype='Select', hidden=1, default="Manufacturing", - options="Manufacturing\nService\nDistribution\nRetail" - ) - ] - } - create_custom_fields(field) diff --git a/erpnext/demo/user/accounts.py b/erpnext/demo/user/accounts.py deleted file mode 100644 index 273a3f92f3b..00000000000 --- a/erpnext/demo/user/accounts.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import random - -import frappe -from frappe.desk import query_report -from frappe.utils import random_string -from frappe.utils.make_random import get_random - -import erpnext -from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry_against_invoice -from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry -from erpnext.accounts.doctype.payment_request.payment_request import ( - make_payment_entry, - make_payment_request, -) -from erpnext.demo.user.sales import make_sales_order -from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice -from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice - - -def work(): - frappe.set_user(frappe.db.get_global('demo_accounts_user')) - - if random.random() <= 0.6: - report = "Ordered Items to be Billed" - for so in list(set([r[0] for r in query_report.run(report)["result"] - if r[0]!="Total"]))[:random.randint(1, 5)]: - try: - si = frappe.get_doc(make_sales_invoice(so)) - si.posting_date = frappe.flags.current_date - for d in si.get("items"): - if not d.income_account: - d.income_account = "Sales - {}".format(frappe.get_cached_value('Company', si.company, 'abbr')) - si.insert() - si.submit() - frappe.db.commit() - except frappe.ValidationError: - pass - - if random.random() <= 0.6: - report = "Received Items to be Billed" - for pr in list(set([r[0] for r in query_report.run(report)["result"] - if r[0]!="Total"]))[:random.randint(1, 5)]: - try: - pi = frappe.get_doc(make_purchase_invoice(pr)) - pi.posting_date = frappe.flags.current_date - pi.bill_no = random_string(6) - pi.insert() - pi.submit() - frappe.db.commit() - except frappe.ValidationError: - pass - - - if random.random() < 0.5: - make_payment_entries("Sales Invoice", "Accounts Receivable") - - if random.random() < 0.5: - make_payment_entries("Purchase Invoice", "Accounts Payable") - - if random.random() < 0.4: - #make payment request against sales invoice - sales_invoice_name = get_random("Sales Invoice", filters={"docstatus": 1}) - if sales_invoice_name: - si = frappe.get_doc("Sales Invoice", sales_invoice_name) - if si.outstanding_amount > 0: - payment_request = make_payment_request(dt="Sales Invoice", dn=si.name, recipient_id=si.contact_email, - submit_doc=True, mute_email=True, use_dummy_message=True) - - payment_entry = frappe.get_doc(make_payment_entry(payment_request.name)) - payment_entry.posting_date = frappe.flags.current_date - payment_entry.submit() - - make_pos_invoice() - -def make_payment_entries(ref_doctype, report): - - outstanding_invoices = frappe.get_all(ref_doctype, fields=["name"], - filters={ - "company": erpnext.get_default_company(), - "outstanding_amount": (">", 0.0) - }) - - # make Payment Entry - for inv in outstanding_invoices[:random.randint(1, 2)]: - pe = get_payment_entry(ref_doctype, inv.name) - pe.posting_date = frappe.flags.current_date - pe.reference_no = random_string(6) - pe.reference_date = frappe.flags.current_date - pe.insert() - pe.submit() - frappe.db.commit() - outstanding_invoices.remove(inv) - - # make payment via JV - for inv in outstanding_invoices[:1]: - jv = frappe.get_doc(get_payment_entry_against_invoice(ref_doctype, inv.name)) - jv.posting_date = frappe.flags.current_date - jv.cheque_no = random_string(6) - jv.cheque_date = frappe.flags.current_date - jv.insert() - jv.submit() - frappe.db.commit() - -def make_pos_invoice(): - make_sales_order() - - for data in frappe.get_all('Sales Order', fields=["name"], - filters = [["per_billed", "<", "100"]]): - si = frappe.get_doc(make_sales_invoice(data.name)) - si.is_pos =1 - si.posting_date = frappe.flags.current_date - for d in si.get("items"): - if not d.income_account: - d.income_account = "Sales - {}".format(frappe.get_cached_value('Company', si.company, 'abbr')) - si.set_missing_values() - make_payment_entries_for_pos_invoice(si) - si.insert() - si.submit() - -def make_payment_entries_for_pos_invoice(si): - for data in si.payments: - data.amount = si.outstanding_amount - return diff --git a/erpnext/demo/user/education.py b/erpnext/demo/user/education.py deleted file mode 100644 index 47519c16642..00000000000 --- a/erpnext/demo/user/education.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import random -from datetime import timedelta - -import frappe -from frappe.utils import cstr -from frappe.utils.make_random import get_random - -from erpnext.education.api import ( - collect_fees, - enroll_student, - get_course, - get_fee_schedule, - get_student_group_students, - make_attendance_records, -) - - -def work(): - frappe.set_user(frappe.db.get_global('demo_education_user')) - for d in range(20): - approve_random_student_applicant() - enroll_random_student(frappe.flags.current_date) - # if frappe.flags.current_date.weekday()== 0: - # make_course_schedule(frappe.flags.current_date, frappe.utils.add_days(frappe.flags.current_date, 5)) - mark_student_attendance(frappe.flags.current_date) - # make_assessment_plan() - make_fees() - -def approve_random_student_applicant(): - random_student = get_random("Student Applicant", {"application_status": "Applied"}) - if random_student: - status = ["Approved", "Rejected"] - frappe.db.set_value("Student Applicant", random_student, "application_status", status[weighted_choice([9,3])]) - -def enroll_random_student(current_date): - batch = ["Section-A", "Section-B"] - random_student = get_random("Student Applicant", {"application_status": "Approved"}) - if random_student: - enrollment = enroll_student(random_student) - enrollment.academic_year = get_random("Academic Year") - enrollment.enrollment_date = current_date - enrollment.student_batch_name = batch[weighted_choice([9,3])] - fee_schedule = get_fee_schedule(enrollment.program) - for fee in fee_schedule: - enrollment.append("fees", fee) - enrolled_courses = get_course(enrollment.program) - for course in enrolled_courses: - enrollment.append("courses", course) - enrollment.submit() - frappe.db.commit() - assign_student_group(enrollment.student, enrollment.student_name, enrollment.program, - enrolled_courses, enrollment.student_batch_name) - -def assign_student_group(student, student_name, program, courses, batch): - course_list = [d["course"] for d in courses] - for d in frappe.get_list("Student Group", fields=("name"), filters={"program": program, "course":("in", course_list), "disabled": 0}): - student_group = frappe.get_doc("Student Group", d.name) - student_group.append("students", {"student": student, "student_name": student_name, - "group_roll_number":len(student_group.students)+1, "active":1}) - student_group.save() - student_batch = frappe.get_list("Student Group", fields=("name"), filters={"program": program, "group_based_on":"Batch", "batch":batch, "disabled": 0})[0] - student_batch_doc = frappe.get_doc("Student Group", student_batch.name) - student_batch_doc.append("students", {"student": student, "student_name": student_name, - "group_roll_number":len(student_batch_doc.students)+1, "active":1}) - student_batch_doc.save() - frappe.db.commit() - -def mark_student_attendance(current_date): - status = ["Present", "Absent"] - for d in frappe.db.get_list("Student Group", filters={"group_based_on": "Batch", "disabled": 0}): - students = get_student_group_students(d.name) - for stud in students: - make_attendance_records(stud.student, stud.student_name, status[weighted_choice([9,4])], None, d.name, current_date) - -def make_fees(): - for d in range(1,10): - random_fee = get_random("Fees", {"paid_amount": 0}) - collect_fees(random_fee, frappe.db.get_value("Fees", random_fee, "outstanding_amount")) - -def make_assessment_plan(date): - for d in range(1,4): - random_group = get_random("Student Group", {"group_based_on": "Course", "disabled": 0}, True) - doc = frappe.new_doc("Assessment Plan") - doc.student_group = random_group.name - doc.course = random_group.course - doc.assessment_group = get_random("Assessment Group", {"is_group": 0, "parent": "2017-18 (Semester 2)"}) - doc.grading_scale = get_random("Grading Scale") - doc.maximum_assessment_score = 100 - -def make_course_schedule(start_date, end_date): - for d in frappe.db.get_list("Student Group"): - cs = frappe.new_doc("Scheduling Tool") - cs.student_group = d.name - cs.room = get_random("Room") - cs.instructor = get_random("Instructor") - cs.course_start_date = cstr(start_date) - cs.course_end_date = cstr(end_date) - day = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] - for x in range(3): - random_day = random.choice(day) - cs.day = random_day - cs.from_time = timedelta(hours=(random.randrange(7, 17,1))) - cs.to_time = cs.from_time + timedelta(hours=1) - cs.schedule_course() - day.remove(random_day) - - -def weighted_choice(weights): - totals = [] - running_total = 0 - - for w in weights: - running_total += w - totals.append(running_total) - - rnd = random.random() * running_total - for i, total in enumerate(totals): - if rnd < total: - return i diff --git a/erpnext/demo/user/fixed_asset.py b/erpnext/demo/user/fixed_asset.py deleted file mode 100644 index 72cd420550a..00000000000 --- a/erpnext/demo/user/fixed_asset.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe -from frappe.utils.make_random import get_random - -from erpnext.assets.doctype.asset.asset import make_sales_invoice -from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset - - -def work(): - frappe.set_user(frappe.db.get_global('demo_accounts_user')) - - # Enable booking asset depreciation entry automatically - frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) - - # post depreciation entries as on today - post_depreciation_entries() - - # scrap a random asset - frappe.db.set_value("Company", "Wind Power LLC", "disposal_account", "Gain/Loss on Asset Disposal - WPL") - - asset = get_random_asset() - scrap_asset(asset.name) - - # Sell a random asset - sell_an_asset() - - -def sell_an_asset(): - asset = get_random_asset() - si = make_sales_invoice(asset.name, asset.item_code, "Wind Power LLC") - si.customer = get_random("Customer") - si.get("items")[0].rate = asset.value_after_depreciation * 0.8 \ - if asset.value_after_depreciation else asset.gross_purchase_amount * 0.9 - si.save() - si.submit() - - -def get_random_asset(): - return frappe.db.sql(""" select name, item_code, value_after_depreciation, gross_purchase_amount - from `tabAsset` - where docstatus=1 and status not in ("Scrapped", "Sold") order by rand() limit 1""", as_dict=1)[0] diff --git a/erpnext/demo/user/hr.py b/erpnext/demo/user/hr.py deleted file mode 100644 index f84a853a791..00000000000 --- a/erpnext/demo/user/hr.py +++ /dev/null @@ -1,223 +0,0 @@ -import datetime -import random - -import frappe -from frappe.utils import add_days, get_last_day, getdate, random_string -from frappe.utils.make_random import get_random - -import erpnext -from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry -from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account -from erpnext.hr.doctype.leave_application.leave_application import ( - AttendanceAlreadyMarkedError, - OverlapError, - get_leave_balance_on, -) -from erpnext.projects.doctype.timesheet.test_timesheet import make_timesheet -from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice - - -def work(): - frappe.set_user(frappe.db.get_global('demo_hr_user')) - year, month = frappe.flags.current_date.strftime("%Y-%m").split("-") - setup_department_approvers() - mark_attendance() - make_leave_application() - - # payroll entry - if not frappe.db.sql('select name from `tabSalary Slip` where month(adddate(start_date, interval 1 month))=month(curdate())'): - # based on frequency - payroll_entry = get_payroll_entry() - payroll_entry.salary_slip_based_on_timesheet = 0 - payroll_entry.save() - payroll_entry.create_salary_slips() - payroll_entry.submit_salary_slips() - payroll_entry.make_accrual_jv_entry() - payroll_entry.submit() - # payroll_entry.make_journal_entry(reference_date=frappe.flags.current_date, - # reference_number=random_string(10)) - - # based on timesheet - payroll_entry = get_payroll_entry() - payroll_entry.salary_slip_based_on_timesheet = 1 - payroll_entry.save() - payroll_entry.create_salary_slips() - payroll_entry.submit_salary_slips() - payroll_entry.make_accrual_jv_entry() - payroll_entry.submit() - # payroll_entry.make_journal_entry(reference_date=frappe.flags.current_date, - # reference_number=random_string(10)) - - if frappe.db.get_global('demo_hr_user'): - make_timesheet_records() - - #expense claim - expense_claim = frappe.new_doc("Expense Claim") - expense_claim.extend('expenses', get_expenses()) - expense_claim.employee = get_random("Employee") - expense_claim.company = frappe.flags.company - expense_claim.payable_account = get_payable_account(expense_claim.company) - expense_claim.posting_date = frappe.flags.current_date - expense_claim.expense_approver = frappe.db.get_global('demo_hr_user') - expense_claim.save() - - rand = random.random() - - if rand < 0.4: - update_sanctioned_amount(expense_claim) - expense_claim.approval_status = 'Approved' - expense_claim.submit() - - if random.randint(0, 1): - #make journal entry against expense claim - je = frappe.get_doc(make_bank_entry("Expense Claim", expense_claim.name)) - je.posting_date = frappe.flags.current_date - je.cheque_no = random_string(10) - je.cheque_date = frappe.flags.current_date - je.flags.ignore_permissions = 1 - je.submit() - -def get_payroll_entry(): - # process payroll for previous month - payroll_entry = frappe.new_doc("Payroll Entry") - payroll_entry.company = frappe.flags.company - payroll_entry.payroll_frequency = 'Monthly' - - # select a posting date from the previous month - payroll_entry.posting_date = get_last_day(getdate(frappe.flags.current_date) - datetime.timedelta(days=10)) - payroll_entry.payment_account = frappe.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name") - - payroll_entry.set_start_end_dates() - return payroll_entry - -def get_expenses(): - expenses = [] - expese_types = frappe.db.sql("""select ect.name, eca.default_account from `tabExpense Claim Type` ect, - `tabExpense Claim Account` eca where eca.parent=ect.name - and eca.company=%s """, frappe.flags.company,as_dict=1) - - for expense_type in expese_types[:random.randint(1,4)]: - claim_amount = random.randint(1,20)*10 - - expenses.append({ - "expense_date": frappe.flags.current_date, - "expense_type": expense_type.name, - "default_account": expense_type.default_account or "Miscellaneous Expenses - WPL", - "amount": claim_amount, - "sanctioned_amount": claim_amount - }) - - return expenses - -def update_sanctioned_amount(expense_claim): - for expense in expense_claim.expenses: - sanctioned_amount = random.randint(1,20)*10 - - if sanctioned_amount < expense.amount: - expense.sanctioned_amount = sanctioned_amount - -def get_timesheet_based_salary_slip_employee(): - sal_struct = frappe.db.sql(""" - select name from `tabSalary Structure` - where salary_slip_based_on_timesheet = 1 - and docstatus != 2""") - if sal_struct: - employees = frappe.db.sql(""" - select employee from `tabSalary Structure Assignment` - where salary_structure IN %(sal_struct)s""", {"sal_struct": sal_struct}, as_dict=True) - return employees - else: - return [] - -def make_timesheet_records(): - employees = get_timesheet_based_salary_slip_employee() - for e in employees: - ts = make_timesheet(e.employee, simulate = True, billable = 1, activity_type=get_random("Activity Type"), company=frappe.flags.company) - frappe.db.commit() - - rand = random.random() - if rand >= 0.3: - make_salary_slip_for_timesheet(ts.name) - - rand = random.random() - if rand >= 0.2: - make_sales_invoice_for_timesheet(ts.name) - -def make_salary_slip_for_timesheet(name): - salary_slip = make_salary_slip(name) - salary_slip.insert() - salary_slip.submit() - frappe.db.commit() - -def make_sales_invoice_for_timesheet(name): - sales_invoice = make_sales_invoice(name) - sales_invoice.customer = get_random("Customer") - sales_invoice.append('items', { - 'item_code': get_random("Item", {"has_variants": 0, "is_stock_item": 0, - "is_fixed_asset": 0}), - 'qty': 1, - 'rate': 1000 - }) - sales_invoice.flags.ignore_permissions = 1 - sales_invoice.set_missing_values() - sales_invoice.calculate_taxes_and_totals() - sales_invoice.insert() - sales_invoice.submit() - frappe.db.commit() - -def make_leave_application(): - allocated_leaves = frappe.get_all("Leave Allocation", fields=['employee', 'leave_type']) - - for allocated_leave in allocated_leaves: - leave_balance = get_leave_balance_on(allocated_leave.employee, allocated_leave.leave_type, frappe.flags.current_date, - consider_all_leaves_in_the_allocation_period=True) - if leave_balance != 0: - if leave_balance == 1: - to_date = frappe.flags.current_date - else: - to_date = add_days(frappe.flags.current_date, random.randint(0, leave_balance-1)) - - leave_application = frappe.get_doc({ - "doctype": "Leave Application", - "employee": allocated_leave.employee, - "from_date": frappe.flags.current_date, - "to_date": to_date, - "leave_type": allocated_leave.leave_type, - }) - try: - leave_application.insert() - leave_application.submit() - frappe.db.commit() - except (OverlapError, AttendanceAlreadyMarkedError): - frappe.db.rollback() - -def mark_attendance(): - attendance_date = frappe.flags.current_date - for employee in frappe.get_all('Employee', fields=['name'], filters = {'status': 'Active'}): - - if not frappe.db.get_value("Attendance", {"employee": employee.name, "attendance_date": attendance_date}): - attendance = frappe.get_doc({ - "doctype": "Attendance", - "employee": employee.name, - "attendance_date": attendance_date - }) - - leave = frappe.db.sql("""select name from `tabLeave Application` - where employee = %s and %s between from_date and to_date - and docstatus = 1""", (employee.name, attendance_date)) - - if leave: - attendance.status = "Absent" - else: - attendance.status = "Present" - attendance.save() - attendance.submit() - frappe.db.commit() - -def setup_department_approvers(): - for d in frappe.get_all('Department', filters={'department_name': ['!=', 'All Departments']}): - doc = frappe.get_doc('Department', d.name) - doc.append("leave_approvers", {'approver': frappe.session.user}) - doc.append("expense_approvers", {'approver': frappe.session.user}) - doc.flags.ignore_mandatory = True - doc.save() diff --git a/erpnext/demo/user/manufacturing.py b/erpnext/demo/user/manufacturing.py deleted file mode 100644 index 6b617761719..00000000000 --- a/erpnext/demo/user/manufacturing.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import random -from datetime import timedelta - -import frappe -from frappe.desk import query_report -from frappe.utils.make_random import how_many - -import erpnext -from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record - - -def work(): - if random.random() < 0.3: return - - frappe.set_user(frappe.db.get_global('demo_manufacturing_user')) - if not frappe.get_all('Sales Order'): return - - ppt = frappe.new_doc("Production Plan") - ppt.company = erpnext.get_default_company() - # ppt.use_multi_level_bom = 1 #refactored - ppt.get_items_from = "Sales Order" - # ppt.purchase_request_for_warehouse = "Stores - WPL" # refactored - ppt.run_method("get_open_sales_orders") - if not ppt.get("sales_orders"): return - ppt.run_method("get_items") - ppt.run_method("raise_material_requests") - ppt.save() - ppt.submit() - ppt.run_method("raise_work_orders") - frappe.db.commit() - - # submit work orders - for pro in frappe.db.get_values("Work Order", {"docstatus": 0}, "name"): - b = frappe.get_doc("Work Order", pro[0]) - b.wip_warehouse = "Work in Progress - WPL" - b.submit() - frappe.db.commit() - - # submit material requests - for pro in frappe.db.get_values("Material Request", {"docstatus": 0}, "name"): - b = frappe.get_doc("Material Request", pro[0]) - b.submit() - frappe.db.commit() - - # stores -> wip - if random.random() < 0.4: - for pro in query_report.run("Open Work Orders")["result"][:how_many("Stock Entry for WIP")]: - make_stock_entry_from_pro(pro[0], "Material Transfer for Manufacture") - - # wip -> fg - if random.random() < 0.4: - for pro in query_report.run("Work Orders in Progress")["result"][:how_many("Stock Entry for FG")]: - make_stock_entry_from_pro(pro[0], "Manufacture") - - for bom in frappe.get_all('BOM', fields=['item'], filters = {'with_operations': 1}): - pro_order = make_wo_order_test_record(item=bom.item, qty=2, - source_warehouse="Stores - WPL", wip_warehouse = "Work in Progress - WPL", - fg_warehouse = "Stores - WPL", company = erpnext.get_default_company(), - stock_uom = frappe.db.get_value('Item', bom.item, 'stock_uom'), - planned_start_date = frappe.flags.current_date) - - # submit job card - if random.random() < 0.4: - submit_job_cards() - -def make_stock_entry_from_pro(pro_id, purpose): - from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry - from erpnext.stock.doctype.stock_entry.stock_entry import ( - DuplicateEntryForWorkOrderError, - IncorrectValuationRateError, - OperationsNotCompleteError, - ) - from erpnext.stock.stock_ledger import NegativeStockError - - try: - st = frappe.get_doc(make_stock_entry(pro_id, purpose)) - st.posting_date = frappe.flags.current_date - st.fiscal_year = str(frappe.flags.current_date.year) - for d in st.get("items"): - d.cost_center = "Main - " + frappe.get_cached_value('Company', st.company, 'abbr') - st.insert() - frappe.db.commit() - st.submit() - frappe.db.commit() - except (NegativeStockError, IncorrectValuationRateError, DuplicateEntryForWorkOrderError, - OperationsNotCompleteError): - frappe.db.rollback() - -def submit_job_cards(): - work_orders = frappe.get_all("Work Order", ["name", "creation"], {"docstatus": 1, "status": "Not Started"}) - work_order = random.choice(work_orders) - # for work_order in work_orders: - start_date = work_order.creation - work_order = frappe.get_doc("Work Order", work_order.name) - job = frappe.get_all("Job Card", ["name", "operation", "work_order"], - {"docstatus": 0, "work_order": work_order.name}) - - if not job: return - job_map = {} - for d in job: - job_map[d.operation] = frappe.get_doc("Job Card", d.name) - - for operation in work_order.operations: - job = job_map[operation.operation] - job_time_log = frappe.new_doc("Job Card Time Log") - job_time_log.from_time = start_date - minutes = operation.get("time_in_mins") - job_time_log.time_in_mins = random.randint(int(minutes/2), minutes) - job_time_log.to_time = job_time_log.from_time + \ - timedelta(minutes=job_time_log.time_in_mins) - job_time_log.parent = job.name - job_time_log.parenttype = 'Job Card' - job_time_log.parentfield = 'time_logs' - job_time_log.completed_qty = work_order.qty - job_time_log.save(ignore_permissions=True) - job.time_logs.append(job_time_log) - job.save(ignore_permissions=True) - job.submit() - start_date = job_time_log.to_time diff --git a/erpnext/demo/user/projects.py b/erpnext/demo/user/projects.py deleted file mode 100644 index 1203be44084..00000000000 --- a/erpnext/demo/user/projects.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe -from frappe.utils import flt -from frappe.utils.make_random import get_random - -import erpnext -from erpnext.demo.user.hr import make_sales_invoice_for_timesheet -from erpnext.projects.doctype.timesheet.test_timesheet import make_timesheet - - -def run_projects(current_date): - frappe.set_user(frappe.db.get_global('demo_projects_user')) - if frappe.db.get_global('demo_projects_user'): - make_project(current_date) - make_timesheet_for_projects(current_date) - close_tasks(current_date) - -def make_timesheet_for_projects(current_date ): - for data in frappe.get_all("Task", ["name", "project"], {"status": "Open", "exp_end_date": ("<", current_date)}): - employee = get_random("Employee") - ts = make_timesheet(employee, simulate = True, billable = 1, company = erpnext.get_default_company(), - activity_type=get_random("Activity Type"), project=data.project, task =data.name) - - if flt(ts.total_billable_amount) > 0.0: - make_sales_invoice_for_timesheet(ts.name) - frappe.db.commit() - -def close_tasks(current_date): - for task in frappe.get_all("Task", ["name"], {"status": "Open", "exp_end_date": ("<", current_date)}): - task = frappe.get_doc("Task", task.name) - task.status = "Completed" - task.save() - -def make_project(current_date): - if not frappe.db.exists('Project', - "New Product Development " + current_date.strftime("%Y-%m-%d")): - project = frappe.get_doc({ - "doctype": "Project", - "project_name": "New Product Development " + current_date.strftime("%Y-%m-%d"), - }) - project.insert() diff --git a/erpnext/demo/user/purchase.py b/erpnext/demo/user/purchase.py deleted file mode 100644 index 61f081c26f9..00000000000 --- a/erpnext/demo/user/purchase.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import json -import random - -import frappe -from frappe.desk import query_report -from frappe.utils.make_random import get_random, how_many - -import erpnext -from erpnext.accounts.party import get_party_account_currency -from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( - make_supplier_quotation_from_rfq, -) -from erpnext.exceptions import InvalidCurrency -from erpnext.setup.utils import get_exchange_rate -from erpnext.stock.doctype.material_request.material_request import make_request_for_quotation - - -def work(): - frappe.set_user(frappe.db.get_global('demo_purchase_user')) - - if random.random() < 0.6: - report = "Items To Be Requested" - for row in query_report.run(report)["result"][:random.randint(1, 5)]: - item_code, qty = row[0], abs(row[-1]) - - mr = make_material_request(item_code, qty) - - if random.random() < 0.6: - for mr in frappe.get_all('Material Request', - filters={'material_request_type': 'Purchase', 'status': 'Open'}, - limit=random.randint(1,6)): - if not frappe.get_all('Request for Quotation', - filters={'material_request': mr.name}, limit=1): - rfq = make_request_for_quotation(mr.name) - rfq.transaction_date = frappe.flags.current_date - add_suppliers(rfq) - rfq.save() - rfq.submit() - - # Make suppier quotation from RFQ against each supplier. - if random.random() < 0.6: - for rfq in frappe.get_all('Request for Quotation', - filters={'status': 'Open'}, limit=random.randint(1, 6)): - if not frappe.get_all('Supplier Quotation', - filters={'request_for_quotation': rfq.name}, limit=1): - rfq = frappe.get_doc('Request for Quotation', rfq.name) - - for supplier in rfq.suppliers: - supplier_quotation = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier.supplier) - supplier_quotation.save() - supplier_quotation.submit() - - # get supplier details - supplier = get_random("Supplier") - - company_currency = frappe.get_cached_value('Company', erpnext.get_default_company(), "default_currency") - party_account_currency = get_party_account_currency("Supplier", supplier, erpnext.get_default_company()) - if company_currency == party_account_currency: - exchange_rate = 1 - else: - exchange_rate = get_exchange_rate(party_account_currency, company_currency, args="for_buying") - - # make supplier quotations - if random.random() < 0.5: - from erpnext.stock.doctype.material_request.material_request import make_supplier_quotation - - report = "Material Requests for which Supplier Quotations are not created" - for row in query_report.run(report)["result"][:random.randint(1, 3)]: - if row[0] != "Total": - sq = frappe.get_doc(make_supplier_quotation(row[0])) - sq.transaction_date = frappe.flags.current_date - sq.supplier = supplier - sq.currency = party_account_currency or company_currency - sq.conversion_rate = exchange_rate - sq.insert() - sq.submit() - frappe.db.commit() - - # make purchase orders - if random.random() < 0.5: - from erpnext.stock.doctype.material_request.material_request import make_purchase_order - report = "Requested Items To Be Ordered" - for row in query_report.run(report)["result"][:how_many("Purchase Order")]: - if row[0] != "Total": - try: - po = frappe.get_doc(make_purchase_order(row[0])) - po.supplier = supplier - po.currency = party_account_currency or company_currency - po.conversion_rate = exchange_rate - po.transaction_date = frappe.flags.current_date - po.insert() - po.submit() - except Exception: - pass - else: - frappe.db.commit() - - if random.random() < 0.5: - make_subcontract() - -def make_material_request(item_code, qty): - mr = frappe.new_doc("Material Request") - - variant_of = frappe.db.get_value('Item', item_code, 'variant_of') or item_code - - if frappe.db.get_value('BOM', {'item': variant_of, 'is_default': 1, 'is_active': 1}): - mr.material_request_type = 'Manufacture' - else: - mr.material_request_type = "Purchase" - - mr.transaction_date = frappe.flags.current_date - mr.schedule_date = frappe.utils.add_days(mr.transaction_date, 7) - - mr.append("items", { - "doctype": "Material Request Item", - "schedule_date": frappe.utils.add_days(mr.transaction_date, 7), - "item_code": item_code, - "qty": qty - }) - mr.insert() - mr.submit() - return mr - -def add_suppliers(rfq): - for i in range(2): - supplier = get_random("Supplier") - if supplier not in [d.supplier for d in rfq.get('suppliers')]: - rfq.append("suppliers", { "supplier": supplier }) - -def make_subcontract(): - from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry - item_code = get_random("Item", {"is_sub_contracted_item": 1}) - if item_code: - # make sub-contract PO - po = frappe.new_doc("Purchase Order") - po.is_subcontracted = "Yes" - po.supplier = get_random("Supplier") - po.transaction_date = frappe.flags.current_date # added - po.schedule_date = frappe.utils.add_days(frappe.flags.current_date, 7) - - item_code = get_random("Item", {"is_sub_contracted_item": 1}) - - po.append("items", { - "item_code": item_code, - "schedule_date": frappe.utils.add_days(frappe.flags.current_date, 7), - "qty": random.randint(10, 30) - }) - po.set_missing_values() - try: - po.insert() - except InvalidCurrency: - return - - po.submit() - - # make material request for - make_material_request(po.items[0].item_code, po.items[0].qty) - - # transfer material for sub-contract - rm_items = get_rm_item(po.items[0], po.supplied_items[0]) - stock_entry = frappe.get_doc(make_rm_stock_entry(po.name, json.dumps([rm_items]))) - stock_entry.from_warehouse = "Stores - WPL" - stock_entry.to_warehouse = "Supplier - WPL" - stock_entry.insert() - -def get_rm_item(items, supplied_items): - return { - "item_code": items.get("item_code"), - "rm_item_code": supplied_items.get("rm_item_code"), - "item_name": supplied_items.get("rm_item_code"), - "qty": supplied_items.get("required_qty") + random.randint(3,10), - "amount": supplied_items.get("amount"), - "warehouse": supplied_items.get("reserve_warehouse"), - "rate": supplied_items.get("rate"), - "stock_uom": supplied_items.get("stock_uom") - } diff --git a/erpnext/demo/user/sales.py b/erpnext/demo/user/sales.py deleted file mode 100644 index ef6e4c42cd8..00000000000 --- a/erpnext/demo/user/sales.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import random - -import frappe -from frappe.utils import flt -from frappe.utils.make_random import add_random_children, get_random - -import erpnext -from erpnext.accounts.doctype.payment_request.payment_request import ( - make_payment_entry, - make_payment_request, -) -from erpnext.accounts.party import get_party_account_currency -from erpnext.setup.utils import get_exchange_rate - - -def work(domain="Manufacturing"): - frappe.set_user(frappe.db.get_global('demo_sales_user_2')) - - for i in range(random.randint(1,7)): - if random.random() < 0.5: - make_opportunity(domain) - - for i in range(random.randint(1,3)): - if random.random() < 0.5: - make_quotation(domain) - - try: - lost_reason = frappe.get_doc({ - "doctype": "Opportunity Lost Reason", - "lost_reason": "Did not ask" - }) - lost_reason.save(ignore_permissions=True) - except frappe.exceptions.DuplicateEntryError: - pass - - # lost quotations / inquiries - if random.random() < 0.3: - for i in range(random.randint(1,3)): - quotation = get_random('Quotation', doc=True) - if quotation and quotation.status == 'Submitted': - quotation.declare_order_lost([{'lost_reason': 'Did not ask'}]) - - for i in range(random.randint(1,3)): - opportunity = get_random('Opportunity', doc=True) - if opportunity and opportunity.status in ('Open', 'Replied'): - opportunity.declare_enquiry_lost([{'lost_reason': 'Did not ask'}]) - - for i in range(random.randint(1,3)): - if random.random() < 0.6: - make_sales_order() - - if random.random() < 0.5: - #make payment request against Sales Order - sales_order_name = get_random("Sales Order", filters={"docstatus": 1}) - try: - if sales_order_name: - so = frappe.get_doc("Sales Order", sales_order_name) - if flt(so.per_billed) != 100: - payment_request = make_payment_request(dt="Sales Order", dn=so.name, recipient_id=so.contact_email, - submit_doc=True, mute_email=True, use_dummy_message=True) - - payment_entry = frappe.get_doc(make_payment_entry(payment_request.name)) - payment_entry.posting_date = frappe.flags.current_date - payment_entry.submit() - except Exception: - pass - -def make_opportunity(domain): - b = frappe.get_doc({ - "doctype": "Opportunity", - "opportunity_from": "Customer", - "party_name": frappe.get_value("Customer", get_random("Customer"), 'name'), - "opportunity_type": "Sales", - "with_items": 1, - "transaction_date": frappe.flags.current_date, - }) - - add_random_children(b, "items", rows=4, randomize = { - "qty": (1, 5), - "item_code": ("Item", {"has_variants": 0, "is_fixed_asset": 0, "domain": domain}) - }, unique="item_code") - - b.insert() - frappe.db.commit() - -def make_quotation(domain): - # get open opportunites - opportunity = get_random("Opportunity", {"status": "Open", "with_items": 1}) - - if opportunity: - from erpnext.crm.doctype.opportunity.opportunity import make_quotation - qtn = frappe.get_doc(make_quotation(opportunity)) - qtn.insert() - frappe.db.commit() - qtn.submit() - frappe.db.commit() - else: - # make new directly - - # get customer, currency and exchange_rate - customer = get_random("Customer") - - company_currency = frappe.get_cached_value('Company', erpnext.get_default_company(), "default_currency") - party_account_currency = get_party_account_currency("Customer", customer, erpnext.get_default_company()) - if company_currency == party_account_currency: - exchange_rate = 1 - else: - exchange_rate = get_exchange_rate(party_account_currency, company_currency, args="for_selling") - - qtn = frappe.get_doc({ - "creation": frappe.flags.current_date, - "doctype": "Quotation", - "quotation_to": "Customer", - "party_name": customer, - "currency": party_account_currency or company_currency, - "conversion_rate": exchange_rate, - "order_type": "Sales", - "transaction_date": frappe.flags.current_date, - }) - - add_random_children(qtn, "items", rows=3, randomize = { - "qty": (1, 5), - "item_code": ("Item", {"has_variants": "0", "is_fixed_asset": 0, "domain": domain}) - }, unique="item_code") - - qtn.insert() - frappe.db.commit() - qtn.submit() - frappe.db.commit() - -def make_sales_order(): - q = get_random("Quotation", {"status": "Submitted"}) - if q: - from erpnext.selling.doctype.quotation.quotation import make_sales_order as mso - so = frappe.get_doc(mso(q)) - so.transaction_date = frappe.flags.current_date - so.delivery_date = frappe.utils.add_days(frappe.flags.current_date, 10) - so.insert() - frappe.db.commit() - so.submit() - frappe.db.commit() diff --git a/erpnext/demo/user/stock.py b/erpnext/demo/user/stock.py deleted file mode 100644 index de379753b3d..00000000000 --- a/erpnext/demo/user/stock.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import random - -import frappe -from frappe.desk import query_report - -import erpnext -from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError -from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return -from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return -from erpnext.stock.doctype.serial_no.serial_no import SerialNoQtyError, SerialNoRequiredError -from erpnext.stock.stock_ledger import NegativeStockError - - -def work(): - frappe.set_user(frappe.db.get_global('demo_manufacturing_user')) - - make_purchase_receipt() - make_delivery_note() - make_stock_reconciliation() - submit_draft_stock_entries() - make_sales_return_records() - make_purchase_return_records() - -def make_purchase_receipt(): - if random.random() < 0.6: - from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt - report = "Purchase Order Items To Be Received" - po_list =list(set([r[0] for r in query_report.run(report)["result"] if r[0]!="Total"]))[:random.randint(1, 10)] - for po in po_list: - pr = frappe.get_doc(make_purchase_receipt(po)) - - if pr.is_subcontracted=="Yes": - pr.supplier_warehouse = "Supplier - WPL" - - pr.posting_date = frappe.flags.current_date - pr.insert() - try: - pr.submit() - except NegativeStockError: - print('Negative stock for {0}'.format(po)) - pass - frappe.db.commit() - -def make_delivery_note(): - # make purchase requests - - # make delivery notes (if possible) - if random.random() < 0.6: - from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note - report = "Ordered Items To Be Delivered" - for so in list(set([r[0] for r in query_report.run(report)["result"] - if r[0]!="Total"]))[:random.randint(1, 3)]: - dn = frappe.get_doc(make_delivery_note(so)) - dn.posting_date = frappe.flags.current_date - for d in dn.get("items"): - if not d.expense_account: - d.expense_account = ("Cost of Goods Sold - {0}".format( - frappe.get_cached_value('Company', dn.company, 'abbr'))) - - try: - dn.insert() - dn.submit() - frappe.db.commit() - except (NegativeStockError, SerialNoRequiredError, SerialNoQtyError, UnableToSelectBatchError): - frappe.db.rollback() - -def make_stock_reconciliation(): - # random set some items as damaged - from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( - EmptyStockReconciliationItemsError, - OpeningEntryAccountError, - ) - - if random.random() < 0.4: - stock_reco = frappe.new_doc("Stock Reconciliation") - stock_reco.posting_date = frappe.flags.current_date - stock_reco.company = erpnext.get_default_company() - stock_reco.get_items_for("Stores - WPL") - if stock_reco.items: - for item in stock_reco.items: - if item.qty: - item.qty = item.qty - round(random.randint(1, item.qty)) - try: - stock_reco.insert(ignore_permissions=True, ignore_mandatory=True) - stock_reco.submit() - frappe.db.commit() - except OpeningEntryAccountError: - frappe.db.rollback() - except EmptyStockReconciliationItemsError: - frappe.db.rollback() - -def submit_draft_stock_entries(): - from erpnext.stock.doctype.stock_entry.stock_entry import ( - DuplicateEntryForWorkOrderError, - IncorrectValuationRateError, - OperationsNotCompleteError, - ) - - # try posting older drafts (if exists) - frappe.db.commit() - for st in frappe.db.get_values("Stock Entry", {"docstatus":0}, "name"): - try: - ste = frappe.get_doc("Stock Entry", st[0]) - ste.posting_date = frappe.flags.current_date - ste.save() - ste.submit() - frappe.db.commit() - except (NegativeStockError, IncorrectValuationRateError, DuplicateEntryForWorkOrderError, - OperationsNotCompleteError): - frappe.db.rollback() - -def make_sales_return_records(): - if random.random() < 0.1: - for data in frappe.get_all('Delivery Note', fields=["name"], filters={"docstatus": 1}): - if random.random() < 0.1: - try: - dn = make_sales_return(data.name) - dn.insert() - dn.submit() - frappe.db.commit() - except Exception: - frappe.db.rollback() - -def make_purchase_return_records(): - if random.random() < 0.1: - for data in frappe.get_all('Purchase Receipt', fields=["name"], filters={"docstatus": 1}): - if random.random() < 0.1: - try: - pr = make_purchase_return(data.name) - pr.insert() - pr.submit() - frappe.db.commit() - except Exception: - frappe.db.rollback() diff --git a/erpnext/domains/hospitality.py b/erpnext/domains/hospitality.py deleted file mode 100644 index 09b98c288bf..00000000000 --- a/erpnext/domains/hospitality.py +++ /dev/null @@ -1,35 +0,0 @@ -data = { - 'desktop_icons': [ - 'Restaurant', - 'Hotels', - 'Accounts', - 'Buying', - 'Stock', - 'HR', - 'Project', - 'ToDo' - ], - 'restricted_roles': [ - 'Restaurant Manager', - 'Hotel Manager', - 'Hotel Reservation User' - ], - 'custom_fields': { - 'Sales Invoice': [ - { - 'fieldname': 'restaurant', 'fieldtype': 'Link', 'options': 'Restaurant', - 'insert_after': 'customer_name', 'label': 'Restaurant', - }, - { - 'fieldname': 'restaurant_table', 'fieldtype': 'Link', 'options': 'Restaurant Table', - 'insert_after': 'restaurant', 'label': 'Restaurant Table', - } - ], - 'Price List': [ - { - 'fieldname':'restaurant_menu', 'fieldtype':'Link', 'options':'Restaurant Menu', 'label':'Restaurant Menu', - 'insert_after':'currency' - } - ] - } -} diff --git a/erpnext/education/api.py b/erpnext/education/api.py index d9013b08161..636b948a1cc 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -201,8 +201,8 @@ def get_course_schedule_events(start, end, filters=None): conditions = get_event_conditions("Course Schedule", filters) data = frappe.db.sql("""select name, course, color, - timestamp(schedule_date, from_time) as from_datetime, - timestamp(schedule_date, to_time) as to_datetime, + timestamp(schedule_date, from_time) as from_time, + timestamp(schedule_date, to_time) as to_time, room, student_group, 0 as 'allDay' from `tabCourse Schedule` where ( schedule_date between %(start)s and %(end)s ) diff --git a/erpnext/education/doctype/course_schedule/course_schedule.py b/erpnext/education/doctype/course_schedule/course_schedule.py index ffd323d3d2a..615d2c47092 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule.py +++ b/erpnext/education/doctype/course_schedule/course_schedule.py @@ -3,6 +3,8 @@ # For license information, please see license.txt +from datetime import datetime + import frappe from frappe import _ from frappe.model.document import Document @@ -30,6 +32,14 @@ class CourseSchedule(Document): if self.from_time > self.to_time: frappe.throw(_("From Time cannot be greater than To Time.")) + """Handles specicfic case to update schedule date in calendar """ + if isinstance(self.from_time, str): + try: + datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S') + self.schedule_date = datetime_obj + except ValueError: + pass + def validate_overlap(self): """Validates overlap for Student Group, Instructor, Room""" @@ -47,4 +57,4 @@ class CourseSchedule(Document): validate_overlap_for(self, "Assessment Plan", "student_group") validate_overlap_for(self, "Assessment Plan", "room") - validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor) + validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor) \ No newline at end of file diff --git a/erpnext/education/doctype/course_schedule/course_schedule_calendar.js b/erpnext/education/doctype/course_schedule/course_schedule_calendar.js index 803527e5480..cacd539b224 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule_calendar.js +++ b/erpnext/education/doctype/course_schedule/course_schedule_calendar.js @@ -1,11 +1,10 @@ frappe.views.calendar["Course Schedule"] = { field_map: { - // from_datetime and to_datetime don't exist as docfields but are used in onload - "start": "from_datetime", - "end": "to_datetime", + "start": "from_time", + "end": "to_time", "id": "name", "title": "course", - "allDay": "allDay" + "allDay": "allDay", }, gantt: false, order_by: "schedule_date", diff --git a/erpnext/education/doctype/course_schedule/test_course_schedule.py b/erpnext/education/doctype/course_schedule/test_course_schedule.py index a7324195557..56149affcea 100644 --- a/erpnext/education/doctype/course_schedule/test_course_schedule.py +++ b/erpnext/education/doctype/course_schedule/test_course_schedule.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.utils import to_timedelta, today +from frappe.utils.data import add_to_date from erpnext.education.utils import OverlapError @@ -39,6 +40,11 @@ class TestCourseSchedule(unittest.TestCase): make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name) + def test_update_schedule_date(self): + doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1)) + doc.schedule_date = add_to_date(doc.schedule_date, days=1) + doc.save() + def make_course_schedule_test_record(**args): args = frappe._dict(args) diff --git a/erpnext/education/workspace/education/education.json b/erpnext/education/workspace/education/education.json index 14652956583..0c7f19894c3 100644 --- a/erpnext/education/workspace/education/education.json +++ b/erpnext/education/workspace/education/education.json @@ -5,7 +5,7 @@ "label": "Program Enrollments" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Education\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Program Enrollments\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Instructor\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Program\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Course\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Fees\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Course Scheduling Tool\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student Attendance Tool\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Student and Instructor\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Content Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Admission\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fees\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Schedule\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"LMS Activity\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assessment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assessment Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Education\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Program Enrollments\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Student\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Instructor\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Program\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Fees\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Student Monthly Attendance Sheet\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Scheduling Tool\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Student Attendance Tool\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Student and Instructor\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Content Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Admission\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fees\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Schedule\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"LMS Activity\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assessment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assessment Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-03-02 17:22:57.066401", "docstatus": 0, "doctype": "Workspace", @@ -692,7 +692,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:15:57.929276", + "modified": "2022-01-13 17:29:13.676542", "modified_by": "Administrator", "module": "Education", "name": "Education", @@ -701,7 +701,7 @@ "public": 1, "restrict_to_domain": "Education", "roles": [], - "sequence_id": 9, + "sequence_id": 9.0, "shortcuts": [ { "color": "Grey", diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py index c5b13c29a27..29bc36f384d 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py @@ -5,11 +5,11 @@ import csv import math import time +from io import StringIO import dateutil import frappe from frappe import _ -from six import StringIO import erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_api as mws diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py index e242ace60f7..a8119ac86cb 100644 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py +++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py @@ -2,13 +2,14 @@ # For license information, please see license.txt +from urllib.parse import urlencode + import frappe import gocardless_pro from frappe import _ from frappe.integrations.utils import create_payment_gateway, create_request_log from frappe.model.document import Document from frappe.utils import call_hook_method, cint, flt, get_url -from six.moves.urllib.parse import urlencode class GoCardlessSettings(Document): diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py index 8da52f49f1c..309d2cb0e37 100644 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py @@ -2,12 +2,13 @@ # For license information, please see license.txt +from urllib.parse import urlparse + import frappe from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.model.document import Document from frappe.utils.nestedset import get_root_of -from six.moves.urllib.parse import urlparse class WoocommerceSettings(Document): diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index d922d875fdd..30d3948fee7 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -1,10 +1,10 @@ import base64 import hashlib import hmac +from urllib.parse import urlparse import frappe from frappe import _ -from six.moves.urllib.parse import urlparse from erpnext import get_default_company diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json index 8e4f92791ab..45077aa66c3 100644 --- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json +++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Marketplace\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payments\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Marketplace\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-08-20 19:30:48.138801", "docstatus": 0, "doctype": "Workspace", @@ -40,17 +40,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Shopify Settings", - "link_count": 0, - "link_to": "Shopify Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -112,7 +101,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:15:58.740247", + "modified": "2022-01-13 17:35:35.508718", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "ERPNext Integrations", @@ -121,7 +110,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 10, + "sequence_id": 10.0, "shortcuts": [], "title": "ERPNext Integrations" } diff --git a/erpnext/hooks.py b/erpnext/hooks.py index e779e0ff279..102e1ccc5e2 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -67,7 +67,6 @@ calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday domains = { 'Distribution': 'erpnext.domains.distribution', 'Education': 'erpnext.domains.education', - 'Hospitality': 'erpnext.domains.hospitality', 'Manufacturing': 'erpnext.domains.manufacturing', 'Non Profit': 'erpnext.domains.non_profit', 'Retail': 'erpnext.domains.retail', @@ -441,6 +440,7 @@ regional_overrides = { 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', + 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount', 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code' }, @@ -576,13 +576,6 @@ global_search_doctypes = { {'doctype': 'Donor Type', 'index': 10}, {'doctype': 'Membership Type', 'index': 11} ], - "Hospitality": [ - {'doctype': 'Hotel Room', 'index': 0}, - {'doctype': 'Hotel Room Reservation', 'index': 1}, - {'doctype': 'Hotel Room Pricing', 'index': 2}, - {'doctype': 'Hotel Room Package', 'index': 3}, - {'doctype': 'Hotel Room Type', 'index': 4} - ] } additional_timeline_content = { diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.js b/erpnext/hotels/doctype/hotel_room/hotel_room.js deleted file mode 100644 index 76f22d5d4ee..00000000000 --- a/erpnext/hotels/doctype/hotel_room/hotel_room.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Hotel Room', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.json b/erpnext/hotels/doctype/hotel_room/hotel_room.json deleted file mode 100644 index 2567c077b63..00000000000 --- a/erpnext/hotels/doctype/hotel_room/hotel_room.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "prompt", - "beta": 1, - "creation": "2017-12-08 12:33:56.320420", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hotel_room_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Hotel Room Type", - "length": 0, - "no_copy": 0, - "options": "Hotel Room Type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "capacity", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Capacity", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "extra_bed_capacity", - "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": "Extra Bed Capacity", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-09 12:10:50.670113", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Hotel Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.py b/erpnext/hotels/doctype/hotel_room/hotel_room.py deleted file mode 100644 index e4bd1c88462..00000000000 --- a/erpnext/hotels/doctype/hotel_room/hotel_room.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe.model.document import Document - - -class HotelRoom(Document): - def validate(self): - if not self.capacity: - self.capacity, self.extra_bed_capacity = frappe.db.get_value('Hotel Room Type', - self.hotel_room_type, ['capacity', 'extra_bed_capacity']) diff --git a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py b/erpnext/hotels/doctype/hotel_room/test_hotel_room.py deleted file mode 100644 index 95efe2c6068..00000000000 --- a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -test_dependencies = ["Hotel Room Package"] -test_records = [ - dict(doctype="Hotel Room", name="1001", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1002", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1003", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1004", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1005", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1006", - hotel_room_type="Basic Room") -] - -class TestHotelRoom(unittest.TestCase): - pass diff --git a/erpnext/hotels/doctype/hotel_room_amenity/__init__.py b/erpnext/hotels/doctype/hotel_room_amenity/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.json b/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.json deleted file mode 100644 index 29a0407a8a8..00000000000 --- a/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-12-08 12:35:36.572185", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 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": 1, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billable", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Billable", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-12-09 12:05:07.125687", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room Amenity", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.py b/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.py deleted file mode 100644 index 166493124a7..00000000000 --- a/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class HotelRoomAmenity(Document): - pass diff --git a/erpnext/hotels/doctype/hotel_room_package/__init__.py b/erpnext/hotels/doctype/hotel_room_package/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.js b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.js deleted file mode 100644 index 5b09ae568ef..00000000000 --- a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Hotel Room Package', { - hotel_room_type: function(frm) { - if (frm.doc.hotel_room_type) { - frappe.model.with_doc('Hotel Room Type', frm.doc.hotel_room_type, () => { - let hotel_room_type = frappe.get_doc('Hotel Room Type', frm.doc.hotel_room_type); - - // reset the amenities - frm.doc.amenities = []; - - for (let amenity of hotel_room_type.amenities) { - let d = frm.add_child('amenities'); - d.item = amenity.item; - d.billable = amenity.billable; - } - - frm.refresh(); - }); - } - } -}); diff --git a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.json b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.json deleted file mode 100644 index 57dad44b7d7..00000000000 --- a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.json +++ /dev/null @@ -1,215 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "prompt", - "beta": 1, - "creation": "2017-12-08 12:43:17.211064", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hotel_room_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Hotel Room Type", - "length": 0, - "no_copy": 0, - "options": "Hotel Room Type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 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, - "unique": 0 - }, - { - "allow_bulk_edit": 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, - "unique": 0 - }, - { - "allow_bulk_edit": 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, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amenities", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amenities", - "length": 0, - "no_copy": 0, - "options": "Hotel Room Amenity", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-09 12:10:31.111952", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room Package", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py deleted file mode 100644 index aedc83a8468..00000000000 --- a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe.model.document import Document - - -class HotelRoomPackage(Document): - def validate(self): - if not self.item: - item = frappe.get_doc(dict( - doctype = 'Item', - item_code = self.name, - item_group = 'Products', - is_stock_item = 0, - stock_uom = 'Unit' - )) - item.insert() - self.item = item.name diff --git a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py deleted file mode 100644 index 749731f4918..00000000000 --- a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -test_records = [ - dict(doctype='Item', item_code='Breakfast', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Lunch', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Dinner', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='WiFi', - item_group='Products', is_stock_item=0), - dict(doctype='Hotel Room Type', name="Delux Room", - capacity=4, - extra_bed_capacity=2, - amenities = [ - dict(item='WiFi', billable=0) - ]), - dict(doctype='Hotel Room Type', name="Basic Room", - capacity=4, - extra_bed_capacity=2, - amenities = [ - dict(item='Breakfast', billable=0) - ]), - dict(doctype="Hotel Room Package", name="Basic Room with Breakfast", - hotel_room_type="Basic Room", - amenities = [ - dict(item="Breakfast", billable=0) - ]), - dict(doctype="Hotel Room Package", name="Basic Room with Lunch", - hotel_room_type="Basic Room", - amenities = [ - dict(item="Breakfast", billable=0), - dict(item="Lunch", billable=0) - ]), - dict(doctype="Hotel Room Package", name="Basic Room with Dinner", - hotel_room_type="Basic Room", - amenities = [ - dict(item="Breakfast", billable=0), - dict(item="Dinner", billable=0) - ]) -] - -class TestHotelRoomPackage(unittest.TestCase): - pass diff --git a/erpnext/hotels/doctype/hotel_room_pricing/__init__.py b/erpnext/hotels/doctype/hotel_room_pricing/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.js b/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.js deleted file mode 100644 index 87bb1925707..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Hotel Room Pricing', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.json b/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.json deleted file mode 100644 index 0f5a776211a..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.json +++ /dev/null @@ -1,266 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "prompt", - "beta": 1, - "creation": "2017-12-08 12:51:47.088174", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fieldname": "enabled", - "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": "Enabled", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "currency", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Currency", - "length": 0, - "no_copy": 0, - "options": "Currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Items", - "length": 0, - "no_copy": 0, - "options": "Hotel Room Pricing 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-09 12:10:41.559559", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room Pricing", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Hotel Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.py b/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.py deleted file mode 100644 index d28e5734264..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class HotelRoomPricing(Document): - pass diff --git a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py b/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py deleted file mode 100644 index 34550096dd9..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -test_dependencies = ["Hotel Room Package"] -test_records = [ - dict(doctype="Hotel Room Pricing", enabled=1, - name="Winter 2017", - from_date="2017-01-01", to_date="2017-01-10", - items = [ - dict(item="Basic Room with Breakfast", rate=10000), - dict(item="Basic Room with Lunch", rate=11000), - dict(item="Basic Room with Dinner", rate=12000) - ]) -] - -class TestHotelRoomPricing(unittest.TestCase): - pass diff --git a/erpnext/hotels/doctype/hotel_room_pricing_item/__init__.py b/erpnext/hotels/doctype/hotel_room_pricing_item/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.json b/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.json deleted file mode 100644 index d6cd826bcc8..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-12-08 12:50:13.486090", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 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": 1, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Rate", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-12-09 12:04:58.641703", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room Pricing Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.py b/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.py deleted file mode 100644 index 2e6bb5fac29..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class HotelRoomPricingItem(Document): - pass diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/__init__.py b/erpnext/hotels/doctype/hotel_room_pricing_package/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.js b/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.js deleted file mode 100644 index f6decd9e95a..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Hotel Room Pricing Package', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.json b/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.json deleted file mode 100644 index 1e529325cd7..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-12-08 12:50:13.486090", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "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": 1, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hotel_room_package", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Hotel Room Package", - "length": 0, - "no_copy": 0, - "options": "Hotel Room Package", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Rate", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-11-04 03:34:02.551811", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room Pricing Package", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.py b/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.py deleted file mode 100644 index ebbdb6ec6c5..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class HotelRoomPricingPackage(Document): - pass diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/test_hotel_room_pricing_package.py b/erpnext/hotels/doctype/hotel_room_pricing_package/test_hotel_room_pricing_package.py deleted file mode 100644 index 196e6504b51..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing_package/test_hotel_room_pricing_package.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestHotelRoomPricingPackage(unittest.TestCase): - pass diff --git a/erpnext/hotels/doctype/hotel_room_reservation/__init__.py b/erpnext/hotels/doctype/hotel_room_reservation/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.js b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.js deleted file mode 100644 index e58d763e474..00000000000 --- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.js +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Hotel Room Reservation', { - refresh: function(frm) { - if(frm.doc.docstatus == 1){ - frm.add_custom_button(__('Create Invoice'), ()=> { - frm.trigger("make_invoice"); - }); - } - }, - from_date: function(frm) { - frm.trigger("recalculate_rates"); - }, - to_date: function(frm) { - frm.trigger("recalculate_rates"); - }, - recalculate_rates: function(frm) { - if (!frm.doc.from_date || !frm.doc.to_date - || !frm.doc.items.length){ - return; - } - frappe.call({ - "method": "erpnext.hotels.doctype.hotel_room_reservation.hotel_room_reservation.get_room_rate", - "args": {"hotel_room_reservation": frm.doc} - }).done((r)=> { - for (var i = 0; i < r.message.items.length; i++) { - frm.doc.items[i].rate = r.message.items[i].rate; - frm.doc.items[i].amount = r.message.items[i].amount; - } - frappe.run_serially([ - ()=> frm.set_value("net_total", r.message.net_total), - ()=> frm.refresh_field("items") - ]); - }); - }, - make_invoice: function(frm) { - frappe.model.with_doc("Hotel Settings", "Hotel Settings", ()=>{ - frappe.model.with_doctype("Sales Invoice", ()=>{ - let hotel_settings = frappe.get_doc("Hotel Settings", "Hotel Settings"); - let invoice = frappe.model.get_new_doc("Sales Invoice"); - invoice.customer = frm.doc.customer || hotel_settings.default_customer; - if (hotel_settings.default_invoice_naming_series){ - invoice.naming_series = hotel_settings.default_invoice_naming_series; - } - for (let d of frm.doc.items){ - let invoice_item = frappe.model.add_child(invoice, "items") - invoice_item.item_code = d.item; - invoice_item.qty = d.qty; - invoice_item.rate = d.rate; - } - if (hotel_settings.default_taxes_and_charges){ - invoice.taxes_and_charges = hotel_settings.default_taxes_and_charges; - } - frappe.set_route("Form", invoice.doctype, invoice.name); - }); - }); - } -}); - -frappe.ui.form.on('Hotel Room Reservation Item', { - item: function(frm, doctype, name) { - frm.trigger("recalculate_rates"); - }, - qty: function(frm) { - frm.trigger("recalculate_rates"); - } -}); diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.json b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.json deleted file mode 100644 index fd20efdf8d7..00000000000 --- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.json +++ /dev/null @@ -1,436 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "HTL-RES-.YYYY.-.#####", - "beta": 1, - "creation": "2017-12-08 13:01:34.829175", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "guest_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Guest Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 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 - }, - { - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "late_checkin", - "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": "Late Checkin", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Booked\nAdvance Paid\nInvoiced\nPaid\nCompleted\nCancelled", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_8", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Items", - "length": 0, - "no_copy": 0, - "options": "Hotel Room Reservation 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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "net_total", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Net Total", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Hotel Room Reservation", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:47.326951", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room Reservation", - "name_case": "", - "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": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Hotel Reservation User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py deleted file mode 100644 index 7725955396b..00000000000 --- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import json - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.utils import add_days, date_diff, flt - - -class HotelRoomUnavailableError(frappe.ValidationError): pass -class HotelRoomPricingNotSetError(frappe.ValidationError): pass - -class HotelRoomReservation(Document): - def validate(self): - self.total_rooms = {} - self.set_rates() - self.validate_availability() - - def validate_availability(self): - for i in range(date_diff(self.to_date, self.from_date)): - day = add_days(self.from_date, i) - self.rooms_booked = {} - - for d in self.items: - if not d.item in self.rooms_booked: - self.rooms_booked[d.item] = 0 - - room_type = frappe.db.get_value("Hotel Room Package", - d.item, 'hotel_room_type') - rooms_booked = get_rooms_booked(room_type, day, exclude_reservation=self.name) \ - + d.qty + self.rooms_booked.get(d.item) - total_rooms = self.get_total_rooms(d.item) - if total_rooms < rooms_booked: - frappe.throw(_("Hotel Rooms of type {0} are unavailable on {1}").format(d.item, - frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomUnavailableError) - - self.rooms_booked[d.item] += rooms_booked - - def get_total_rooms(self, item): - if not item in self.total_rooms: - self.total_rooms[item] = frappe.db.sql(""" - select count(*) - from - `tabHotel Room Package` package - inner join - `tabHotel Room` room on package.hotel_room_type = room.hotel_room_type - where - package.item = %s""", item)[0][0] or 0 - - return self.total_rooms[item] - - def set_rates(self): - self.net_total = 0 - for d in self.items: - net_rate = 0.0 - for i in range(date_diff(self.to_date, self.from_date)): - day = add_days(self.from_date, i) - if not d.item: - continue - day_rate = frappe.db.sql(""" - select - item.rate - from - `tabHotel Room Pricing Item` item, - `tabHotel Room Pricing` pricing - where - item.parent = pricing.name - and item.item = %s - and %s between pricing.from_date - and pricing.to_date""", (d.item, day)) - - if day_rate: - net_rate += day_rate[0][0] - else: - frappe.throw( - _("Please set Hotel Room Rate on {}").format( - frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomPricingNotSetError) - d.rate = net_rate - d.amount = net_rate * flt(d.qty) - self.net_total += d.amount - -@frappe.whitelist() -def get_room_rate(hotel_room_reservation): - """Calculate rate for each day as it may belong to different Hotel Room Pricing Item""" - doc = frappe.get_doc(json.loads(hotel_room_reservation)) - doc.set_rates() - return doc.as_dict() - -def get_rooms_booked(room_type, day, exclude_reservation=None): - exclude_condition = '' - if exclude_reservation: - exclude_condition = 'and reservation.name != {0}'.format(frappe.db.escape(exclude_reservation)) - - return frappe.db.sql(""" - select sum(item.qty) - from - `tabHotel Room Package` room_package, - `tabHotel Room Reservation Item` item, - `tabHotel Room Reservation` reservation - where - item.parent = reservation.name - and room_package.item = item.item - and room_package.hotel_room_type = %s - and reservation.docstatus = 1 - {exclude_condition} - and %s between reservation.from_date - and reservation.to_date""".format(exclude_condition=exclude_condition), - (room_type, day))[0][0] or 0 diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js deleted file mode 100644 index 7bde292a2bc..00000000000 --- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js +++ /dev/null @@ -1,9 +0,0 @@ -frappe.views.calendar["Hotel Room Reservation"] = { - field_map: { - "start": "from_date", - "end": "to_date", - "id": "name", - "title": "guest_name", - "status": "status" - } -} diff --git a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py deleted file mode 100644 index bb32a27fa7c..00000000000 --- a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -import frappe - -from erpnext.hotels.doctype.hotel_room_reservation.hotel_room_reservation import ( - HotelRoomPricingNotSetError, - HotelRoomUnavailableError, -) - -test_dependencies = ["Hotel Room Package", "Hotel Room Pricing", "Hotel Room"] - -class TestHotelRoomReservation(unittest.TestCase): - def setUp(self): - frappe.db.sql("delete from `tabHotel Room Reservation`") - frappe.db.sql("delete from `tabHotel Room Reservation Item`") - - def test_reservation(self): - reservation = make_reservation( - from_date="2017-01-01", - to_date="2017-01-03", - items=[ - dict(item="Basic Room with Dinner", qty=2) - ] - ) - reservation.insert() - self.assertEqual(reservation.net_total, 48000) - - def test_price_not_set(self): - reservation = make_reservation( - from_date="2016-01-01", - to_date="2016-01-03", - items=[ - dict(item="Basic Room with Dinner", qty=2) - ] - ) - self.assertRaises(HotelRoomPricingNotSetError, reservation.insert) - - def test_room_unavailable(self): - reservation = make_reservation( - from_date="2017-01-01", - to_date="2017-01-03", - items=[ - dict(item="Basic Room with Dinner", qty=2), - ] - ) - reservation.insert() - - reservation = make_reservation( - from_date="2017-01-01", - to_date="2017-01-03", - items=[ - dict(item="Basic Room with Dinner", qty=20), - ] - ) - self.assertRaises(HotelRoomUnavailableError, reservation.insert) - -def make_reservation(**kwargs): - kwargs["doctype"] = "Hotel Room Reservation" - if not "guest_name" in kwargs: - kwargs["guest_name"] = "Test Guest" - doc = frappe.get_doc(kwargs) - return doc diff --git a/erpnext/hotels/doctype/hotel_room_reservation_item/__init__.py b/erpnext/hotels/doctype/hotel_room_reservation_item/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.json b/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.json deleted file mode 100644 index 2b7931ebc0f..00000000000 --- a/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.json +++ /dev/null @@ -1,195 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-12-08 12:58:21.733330", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 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": 1, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "qty", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Qty", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "currency", - "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": "Currency", - "length": 0, - "no_copy": 0, - "options": "Currency", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Rate", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Amount", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-12-09 12:04:34.562956", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room Reservation Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.py b/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.py deleted file mode 100644 index 41d86ddca65..00000000000 --- a/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class HotelRoomReservationItem(Document): - pass diff --git a/erpnext/hotels/doctype/hotel_room_type/__init__.py b/erpnext/hotels/doctype/hotel_room_type/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.js b/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.js deleted file mode 100644 index d73835db947..00000000000 --- a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Hotel Room Type', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.json b/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.json deleted file mode 100644 index 3d26413cf8e..00000000000 --- a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "prompt", - "beta": 1, - "creation": "2017-12-08 12:38:29.485175", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "capacity", - "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": "Capacity", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "extra_bed_capacity", - "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": "Extra Bed Capacity", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_3", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amenities", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amenities", - "length": 0, - "no_copy": 0, - "options": "Hotel Room Amenity", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-09 12:10:23.355486", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room Type", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Hotel Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.py b/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.py deleted file mode 100644 index 7ab529fee96..00000000000 --- a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class HotelRoomType(Document): - pass diff --git a/erpnext/hotels/doctype/hotel_room_type/test_hotel_room_type.py b/erpnext/hotels/doctype/hotel_room_type/test_hotel_room_type.py deleted file mode 100644 index 8d1147d0f20..00000000000 --- a/erpnext/hotels/doctype/hotel_room_type/test_hotel_room_type.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestHotelRoomType(unittest.TestCase): - pass diff --git a/erpnext/hotels/doctype/hotel_settings/__init__.py b/erpnext/hotels/doctype/hotel_settings/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/doctype/hotel_settings/hotel_settings.js b/erpnext/hotels/doctype/hotel_settings/hotel_settings.js deleted file mode 100644 index 0b4a2c36ca2..00000000000 --- a/erpnext/hotels/doctype/hotel_settings/hotel_settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Hotel Settings', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/hotels/doctype/hotel_settings/hotel_settings.json b/erpnext/hotels/doctype/hotel_settings/hotel_settings.json deleted file mode 100644 index d9f5572549e..00000000000 --- a/erpnext/hotels/doctype/hotel_settings/hotel_settings.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 1, - "creation": "2017-12-08 17:50:24.523107", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_taxes_and_charges", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Taxes and Charges", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_invoice_naming_series", - "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": "Default Invoice Naming Series", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-09 12:11:12.857308", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Settings", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Hotel Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hotels/doctype/hotel_settings/hotel_settings.py b/erpnext/hotels/doctype/hotel_settings/hotel_settings.py deleted file mode 100644 index 8376d509693..00000000000 --- a/erpnext/hotels/doctype/hotel_settings/hotel_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class HotelSettings(Document): - pass diff --git a/erpnext/hotels/doctype/hotel_settings/test_hotel_settings.py b/erpnext/hotels/doctype/hotel_settings/test_hotel_settings.py deleted file mode 100644 index e76c00ce101..00000000000 --- a/erpnext/hotels/doctype/hotel_settings/test_hotel_settings.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestHotelSettings(unittest.TestCase): - pass diff --git a/erpnext/hotels/report/__init__.py b/erpnext/hotels/report/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/report/hotel_room_occupancy/__init__.py b/erpnext/hotels/report/hotel_room_occupancy/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.js b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.js deleted file mode 100644 index 81efb2dd120..00000000000 --- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.js +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt -/* eslint-disable */ - -frappe.query_reports["Hotel Room Occupancy"] = { - "filters": [ - { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - "default": frappe.datetime.now_date(), - "reqd":1 - }, - { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.now_date(), - "reqd":1 - } - ] -} diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.json b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.json deleted file mode 100644 index 782a48bbb9d..00000000000 --- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2017-12-09 14:31:26.306705", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2017-12-09 14:31:26.306705", - "modified_by": "Administrator", - "module": "Hotels", - "name": "Hotel Room Occupancy", - "owner": "Administrator", - "ref_doctype": "Hotel Room Reservation", - "report_name": "Hotel Room Occupancy", - "report_type": "Script Report", - "roles": [ - { - "role": "System Manager" - }, - { - "role": "Hotel Reservation User" - } - ] -} \ No newline at end of file diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py deleted file mode 100644 index c43589d2a8d..00000000000 --- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.utils import add_days, date_diff - -from erpnext.hotels.doctype.hotel_room_reservation.hotel_room_reservation import get_rooms_booked - - -def execute(filters=None): - columns = get_columns(filters) - data = get_data(filters) - return columns, data - -def get_columns(filters): - columns = [ - dict(label=_("Room Type"), fieldname="room_type"), - dict(label=_("Rooms Booked"), fieldtype="Int") - ] - return columns - -def get_data(filters): - out = [] - for room_type in frappe.get_all('Hotel Room Type'): - total_booked = 0 - for i in range(date_diff(filters.to_date, filters.from_date)): - day = add_days(filters.from_date, i) - total_booked += get_rooms_booked(room_type.name, day) - - out.append([room_type.name, total_booked]) - - return out diff --git a/erpnext/hr/doctype/appointment_letter/appointment_letter.json b/erpnext/hr/doctype/appointment_letter/appointment_letter.json index c81b7004f63..012f6b6b494 100644 --- a/erpnext/hr/doctype/appointment_letter/appointment_letter.json +++ b/erpnext/hr/doctype/appointment_letter/appointment_letter.json @@ -86,11 +86,12 @@ } ], "links": [], - "modified": "2020-01-21 17:30:36.334395", + "modified": "2022-01-18 19:27:35.649424", "modified_by": "Administrator", "module": "HR", "name": "Appointment Letter", "name_case": "Title Case", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -118,7 +119,10 @@ "write": 1 } ], + "search_fields": "applicant_name, company", "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "applicant_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/appointment_letter_template/appointment_letter_template.json b/erpnext/hr/doctype/appointment_letter_template/appointment_letter_template.json index c136fb22fab..5e50fe6d8f2 100644 --- a/erpnext/hr/doctype/appointment_letter_template/appointment_letter_template.json +++ b/erpnext/hr/doctype/appointment_letter_template/appointment_letter_template.json @@ -1,11 +1,12 @@ { "actions": [], - "autoname": "HR-APP-LETTER-TEMP-.#####", + "autoname": "field:template_name", "creation": "2019-12-26 12:20:14.219578", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "template_name", "introduction", "terms", "closing_notes" @@ -29,13 +30,21 @@ "label": "Terms", "options": "Appointment Letter content", "reqd": 1 + }, + { + "fieldname": "template_name", + "fieldtype": "Data", + "label": "Template Name", + "reqd": 1, + "unique": 1 } ], "links": [], - "modified": "2020-01-21 17:00:46.779420", + "modified": "2022-01-18 19:25:14.614616", "modified_by": "Administrator", "module": "HR", "name": "Appointment Letter Template", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -63,7 +72,10 @@ "write": 1 } ], + "search_fields": "template_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "template_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 7dcfac249f4..b1eaaf8b587 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -5,9 +5,9 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, formatdate, get_datetime, getdate, nowdate +from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate -from erpnext.hr.utils import validate_active_employee +from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee class Attendance(Document): @@ -171,7 +171,7 @@ def get_month_map(): }) @frappe.whitelist() -def get_unmarked_days(employee, month): +def get_unmarked_days(employee, month, exclude_holidays=0): import calendar month_map = get_month_map() @@ -191,6 +191,11 @@ def get_unmarked_days(employee, month): ]) marked_days = [get_datetime(record.attendance_date) for record in records] + if cint(exclude_holidays): + holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end) + holidays = [get_datetime(record) for record in holiday_dates] + marked_days.extend(holidays) + unmarked_days = [] for date in dates_of_month: diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 6b3c29a76b4..3a5c5915396 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -28,6 +28,7 @@ frappe.listview_settings['Attendance'] = { onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); dialog.set_df_property("status", "hidden", 1); + dialog.set_df_property("exclude_holidays", "hidden", 1); dialog.set_df_property("month", "value", ''); dialog.set_df_property("unmarked_days", "options", []); dialog.no_unmarked_days_left = false; @@ -42,9 +43,14 @@ frappe.listview_settings['Attendance'] = { onchange: function() { if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { dialog.set_df_property("status", "hidden", 0); + dialog.set_df_property("exclude_holidays", "hidden", 0); dialog.set_df_property("unmarked_days", "options", []); dialog.no_unmarked_days_left = false; - me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => { + me.get_multi_select_options( + dialog.fields_dict.employee.value, + dialog.fields_dict.month.value, + dialog.fields_dict.exclude_holidays.get_value() + ).then(options => { if (options.length > 0) { dialog.set_df_property("unmarked_days", "hidden", 0); dialog.set_df_property("unmarked_days", "options", options); @@ -64,6 +70,31 @@ frappe.listview_settings['Attendance'] = { reqd: 1, }, + { + label: __("Exclude Holidays"), + fieldtype: "Check", + fieldname: "exclude_holidays", + hidden: 1, + onchange: function() { + if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { + dialog.set_df_property("status", "hidden", 0); + dialog.set_df_property("unmarked_days", "options", []); + dialog.no_unmarked_days_left = false; + me.get_multi_select_options( + dialog.fields_dict.employee.value, + dialog.fields_dict.month.value, + dialog.fields_dict.exclude_holidays.get_value() + ).then(options => { + if (options.length > 0) { + dialog.set_df_property("unmarked_days", "hidden", 0); + dialog.set_df_property("unmarked_days", "options", options); + } else { + dialog.no_unmarked_days_left = true; + } + }); + } + } + }, { label: __("Unmarked Attendance for days"), fieldname: "unmarked_days", @@ -105,7 +136,7 @@ frappe.listview_settings['Attendance'] = { }); }, - get_multi_select_options: function(employee, month) { + get_multi_select_options: function(employee, month, exclude_holidays) { return new Promise(resolve => { frappe.call({ method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days', @@ -113,6 +144,7 @@ frappe.listview_settings['Attendance'] = { args: { employee: employee, month: month, + exclude_holidays: exclude_holidays } }).then(r => { var options = []; diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index a1247d9eb17..fb62b0414d0 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -21,7 +21,7 @@ def get_data(): }, { 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance'] + 'items': ['Employee Onboarding', 'Employee Transfer', 'Employee Promotion', 'Employee Grievance'] }, { 'label': _('Exit'), diff --git a/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json b/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json index 044a5a9886b..8474bd09d51 100644 --- a/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json +++ b/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json @@ -62,6 +62,7 @@ }, { "default": "0", + "depends_on": "eval:['Employee Onboarding', 'Employee Onboarding Template'].includes(doc.parenttype)", "description": "Applicable in the case of Employee Onboarding", "fieldname": "required_for_employee_creation", "fieldtype": "Check", @@ -93,7 +94,7 @@ ], "istable": 1, "links": [], - "modified": "2021-07-30 15:55:22.470102", + "modified": "2022-01-29 14:05:00.543122", "modified_by": "Administrator", "module": "HR", "name": "Employee Boarding Activity", @@ -102,5 +103,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js index 5d1a024ebb3..6fbb54d002a 100644 --- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js +++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js @@ -3,12 +3,6 @@ frappe.ui.form.on('Employee Onboarding', { setup: function(frm) { - frm.add_fetch("employee_onboarding_template", "company", "company"); - frm.add_fetch("employee_onboarding_template", "department", "department"); - frm.add_fetch("employee_onboarding_template", "designation", "designation"); - frm.add_fetch("employee_onboarding_template", "employee_grade", "employee_grade"); - - frm.set_query("job_applicant", function () { return { filters:{ @@ -71,5 +65,19 @@ frappe.ui.form.on('Employee Onboarding', { } }); } + }, + + job_applicant: function(frm) { + if (frm.doc.job_applicant) { + frappe.db.get_value('Employee', {'job_applicant': frm.doc.job_applicant}, 'name', (r) => { + if (r.name) { + frm.set_value('employee', r.name); + } else { + frm.set_value('employee', ''); + } + }); + } else { + frm.set_value('employee', ''); + } } }); diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json index fd877a68d85..1d2ea0c669a 100644 --- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json +++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json @@ -92,6 +92,7 @@ "options": "Employee Onboarding Template" }, { + "fetch_from": "employee_onboarding_template.company", "fieldname": "company", "fieldtype": "Link", "label": "Company", @@ -99,6 +100,7 @@ "reqd": 1 }, { + "fetch_from": "employee_onboarding_template.department", "fieldname": "department", "fieldtype": "Link", "in_list_view": 1, @@ -106,6 +108,7 @@ "options": "Department" }, { + "fetch_from": "employee_onboarding_template.designation", "fieldname": "designation", "fieldtype": "Link", "in_list_view": 1, @@ -113,6 +116,7 @@ "options": "Designation" }, { + "fetch_from": "employee_onboarding_template.employee_grade", "fieldname": "employee_grade", "fieldtype": "Link", "label": "Employee Grade", @@ -170,10 +174,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-07-30 14:55:04.560683", + "modified": "2022-01-29 12:33:57.120384", "modified_by": "Administrator", "module": "HR", "name": "Employee Onboarding", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -194,6 +199,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py index eba2a03193c..a0939a847b2 100644 --- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py @@ -14,10 +14,15 @@ class IncompleteTaskError(frappe.ValidationError): pass class EmployeeOnboarding(EmployeeBoardingController): def validate(self): super(EmployeeOnboarding, self).validate() + self.set_employee() self.validate_duplicate_employee_onboarding() + def set_employee(self): + if not self.employee: + self.employee = frappe.db.get_value('Employee', {'job_applicant': self.job_applicant}, 'name') + def validate_duplicate_employee_onboarding(self): - emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant}) + emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant, "docstatus": ("!=", 2)}) if emp_onboarding and emp_onboarding != self.name: frappe.throw(_("Employee Onboarding: {0} already exists for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant))) diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py index cb1b56048b7..2d129c8acfc 100644 --- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py @@ -19,7 +19,7 @@ class TestEmployeeOnboarding(unittest.TestCase): if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}): frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'}) - project = "Employee Onboarding : Test Researcher - test@researcher.com" + project = "Employee Onboarding : test@researcher.com" frappe.db.sql("delete from tabProject where name=%s", project) frappe.db.sql("delete from tabTask where project=%s", project) @@ -27,7 +27,7 @@ class TestEmployeeOnboarding(unittest.TestCase): onboarding = create_employee_onboarding() project_name = frappe.db.get_value('Project', onboarding.project, 'project_name') - self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com') + self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com') # don't allow making employee if onboarding is not complete self.assertRaises(IncompleteTaskError, make_employee, onboarding.name) @@ -64,8 +64,8 @@ class TestEmployeeOnboarding(unittest.TestCase): def get_job_applicant(): - if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'): - return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com') + if frappe.db.exists('Job Applicant', 'test@researcher.com'): + return frappe.get_doc('Job Applicant', 'test@researcher.com') applicant = frappe.new_doc('Job Applicant') applicant.applicant_name = 'Test Researcher' applicant.email_id = 'test@researcher.com' diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py index 4bb003ded18..a3b111ccb15 100644 --- a/erpnext/hr/doctype/interview/interview.py +++ b/erpnext/hr/doctype/interview/interview.py @@ -7,7 +7,7 @@ import datetime import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, get_datetime, get_link_to_form +from frappe.utils import cstr, flt, get_datetime, get_link_to_form class DuplicateInterviewRoundError(frappe.ValidationError): @@ -18,6 +18,7 @@ class Interview(Document): self.validate_duplicate_interview() self.validate_designation() self.validate_overlap() + self.set_average_rating() def on_submit(self): if self.status not in ['Cleared', 'Rejected']: @@ -67,6 +68,13 @@ class Interview(Document): overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0])) frappe.throw(overlapping_details, title=_('Overlap')) + def set_average_rating(self): + total_rating = 0 + for entry in self.interview_details: + if entry.average_rating: + total_rating += entry.average_rating + + self.average_rating = flt(total_rating / len(self.interview_details) if len(self.interview_details) else 0) @frappe.whitelist() def reschedule_interview(self, scheduled_on, from_time, to_time): diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py index 1a2257a6d90..fdb11afe822 100644 --- a/erpnext/hr/doctype/interview/test_interview.py +++ b/erpnext/hr/doctype/interview/test_interview.py @@ -12,6 +12,7 @@ from frappe.utils import add_days, getdate, nowtime from erpnext.hr.doctype.designation.test_designation import create_designation from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError +from erpnext.hr.doctype.job_applicant.job_applicant import get_interview_details from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant @@ -70,6 +71,20 @@ class TestInterview(unittest.TestCase): email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message) + def test_get_interview_details_for_applicant_dashboard(self): + job_applicant = create_job_applicant() + interview = create_interview_and_dependencies(job_applicant.name) + + details = get_interview_details(job_applicant.name) + self.assertEqual(details.get('stars'), 5) + self.assertEqual(details.get('interviews').get(interview.name), { + 'name': interview.name, + 'interview_round': interview.interview_round, + 'expected_average_rating': interview.expected_average_rating * 5, + 'average_rating': interview.average_rating * 5, + 'status': 'Pending' + }) + def tearDown(self): frappe.db.rollback() @@ -106,7 +121,8 @@ def create_interview_round(name, skill_set, interviewers=[], designation=None, s interview_round = frappe.new_doc("Interview Round") interview_round.round_name = name interview_round.interview_type = create_interview_type() - interview_round.expected_average_rating = 4 + # average rating = 4 + interview_round.expected_average_rating = 0.8 if designation: interview_round.designation = designation diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.py b/erpnext/hr/doctype/interview_feedback/interview_feedback.py index d046458f196..2ff00c1cac7 100644 --- a/erpnext/hr/doctype/interview_feedback/interview_feedback.py +++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.py @@ -57,7 +57,6 @@ class InterviewFeedback(Document): def update_interview_details(self): doc = frappe.get_doc('Interview', self.interview) - total_rating = 0 if self.docstatus == 2: for entry in doc.interview_details: @@ -72,10 +71,6 @@ class InterviewFeedback(Document): entry.comments = self.feedback entry.result = self.result - if entry.average_rating: - total_rating += entry.average_rating - - doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0) doc.save() doc.notify_update() diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py index d2ec5b9438e..19c464296ac 100644 --- a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py +++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py @@ -24,7 +24,7 @@ class TestInterviewFeedback(unittest.TestCase): create_skill_set(['Leadership']) interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings) - interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4}) + interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 0.8}) frappe.set_user(interviewer) self.assertRaises(frappe.ValidationError, interview_feedback.save) @@ -50,7 +50,7 @@ class TestInterviewFeedback(unittest.TestCase): avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0) - self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating) + self.assertEqual(flt(avg_rating, 2), flt(feedback_1.average_rating, 2)) avg_on_interview_detail = frappe.db.get_value('Interview Detail', { 'parent': feedback_1.interview, @@ -59,7 +59,7 @@ class TestInterviewFeedback(unittest.TestCase): }, 'average_rating') # 1. average should be reflected in Interview Detail. - self.assertEqual(avg_on_interview_detail, feedback_1.average_rating) + self.assertEqual(flt(avg_on_interview_detail, 2), flt(feedback_1.average_rating, 2)) '''For Second Interviewer Feedback''' interviewer = interview.interview_details[1].interviewer @@ -97,5 +97,5 @@ def get_skills_rating(interview_round): skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"]) for d in skills: - d["rating"] = random.randint(1, 5) + d["rating"] = random.random() return skills diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.js b/erpnext/hr/doctype/job_applicant/job_applicant.js index d7b1c6c9df3..c1e82571683 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.js +++ b/erpnext/hr/doctype/job_applicant/job_applicant.js @@ -21,9 +21,9 @@ frappe.ui.form.on("Job Applicant", { create_custom_buttons: function(frm) { if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") { - frm.add_custom_button(__("Create Interview"), function() { + frm.add_custom_button(__("Interview"), function() { frm.events.create_dialog(frm); - }); + }, __("Create")); } if (!frm.doc.__islocal) { @@ -40,10 +40,10 @@ frappe.ui.form.on("Job Applicant", { frappe.route_options = { "job_applicant": frm.doc.name, "applicant_name": frm.doc.applicant_name, - "designation": frm.doc.job_opening, + "designation": frm.doc.job_opening || frm.doc.designation, }; frappe.new_doc("Job Offer"); - }); + }, __("Create")); } } }, @@ -55,13 +55,16 @@ frappe.ui.form.on("Job Applicant", { job_applicant: frm.doc.name }, callback: function(r) { - $("div").remove(".form-dashboard-section.custom"); - frm.dashboard.add_section( - frappe.render_template('job_applicant_dashboard', { - data: r.message - }), - __("Interview Summary") - ); + if (r.message) { + $("div").remove(".form-dashboard-section.custom"); + frm.dashboard.add_section( + frappe.render_template("job_applicant_dashboard", { + data: r.message.interviews, + number_of_stars: r.message.stars + }), + __("Interview Summary") + ); + } } }); }, diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json index 200f675221b..66b609cf990 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.json +++ b/erpnext/hr/doctype/job_applicant/job_applicant.json @@ -192,10 +192,11 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-09-29 23:06:10.904260", + "modified": "2022-01-12 16:28:53.196881", "modified_by": "Administrator", "module": "HR", "name": "Job Applicant", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -210,10 +211,11 @@ "write": 1 } ], - "search_fields": "applicant_name", + "search_fields": "applicant_name, email_id, job_title, phone_number", "sender_field": "email_id", "sort_field": "modified", "sort_order": "ASC", + "states": [], "subject_field": "notes", "title_field": "applicant_name" } \ No newline at end of file diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py index abaa50c84ce..ccc21ced2cc 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.model.naming import append_number_if_name_exists from frappe.utils import validate_email_address from erpnext.hr.doctype.interview.interview import get_interviewers @@ -21,10 +22,11 @@ class JobApplicant(Document): self.get("__onload").job_offer = job_offer[0].name def autoname(self): - keys = filter(None, (self.applicant_name, self.email_id, self.job_title)) - if not keys: - frappe.throw(_("Name or Email is mandatory"), frappe.NameError) - self.name = " - ".join(keys) + self.name = self.email_id + + # applicant can apply more than once for a different job title or reapply + if frappe.db.exists("Job Applicant", self.name): + self.name = append_number_if_name_exists("Job Applicant", self.name) def validate(self): if self.email_id: @@ -79,8 +81,13 @@ def get_interview_details(job_applicant): fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"] ) interview_detail_map = {} + meta = frappe.get_meta("Interview") + number_of_stars = meta.get_options("expected_average_rating") or 5 for detail in interview_details: + detail.expected_average_rating = detail.expected_average_rating * number_of_stars if detail.expected_average_rating else 0 + detail.average_rating = detail.average_rating * number_of_stars if detail.average_rating else 0 + interview_detail_map[detail.name] = detail - return interview_detail_map + return {"interviews": interview_detail_map, "stars": number_of_stars} diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html index c286787a556..734b2fe5e8b 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html +++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html @@ -17,24 +17,33 @@ {%= key %} {%= value["interview_round"] %} {%= value["status"] %} - - {% for (i = 0; i < value["expected_average_rating"]; i++) { %} - - {% } %} - {% for (i = 0; i < (5-value["expected_average_rating"]); i++) { %} - - {% } %} - - - {% if(value["average_rating"]){ %} - {% for (i = 0; i < value["average_rating"]; i++) { %} - - {% } %} - {% for (i = 0; i < (5-value["average_rating"]); i++) { %} - - {% } %} - {% } %} - + {% let right_class = ''; %} + {% let left_class = ''; %} + + {% $.each([value["expected_average_rating"], value["average_rating"]], (_, val) => { %} + +
+ {% for (let i = 1; i <= number_of_stars; i++) { %} + {% if (i <= val) { %} + {% right_class = 'star-click'; %} + {% } else { %} + {% right_class = ''; %} + {% } %} + + {% if ((i <= val) || ((i - 0.5) == val)) { %} + {% left_class = 'star-click'; %} + {% } else { %} + {% left_class = ''; %} + {% } %} + + + + + + {% } %} +
+ + {% }); %} {% } %} diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py index 36dcf6b0740..bf1622028d8 100644 --- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py @@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation class TestJobApplicant(unittest.TestCase): - pass + def test_job_applicant_naming(self): + applicant = frappe.get_doc({ + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com" + }).insert() + self.assertEqual(applicant.name, 'job_applicant_naming@example.com') + + applicant = frappe.get_doc({ + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com" + }).insert() + self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1') + + def tearDown(self): + frappe.db.rollback() + def create_job_applicant(**args): args = frappe._dict(args) diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index 39f471929b4..072fc73271f 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -78,6 +78,7 @@ def make_employee(source_name, target_doc=None): "doctype": "Employee", "field_map": { "applicant_name": "employee_name", + "offer_date": "scheduled_confirmation_date" }} }, target_doc, set_missing_values) return doc diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 52ee463db02..9ecbe014b97 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -237,10 +237,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-10-01 15:28:26.335104", + "modified": "2022-01-18 19:15:53.262536", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -278,5 +279,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "timeline_field": "employee" -} + "states": [], + "timeline_field": "employee", + "title_field": "employee_name" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 1dc5b31461e..70250f5bcf8 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -22,6 +22,7 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry from erpnext.hr.utils import ( + get_holiday_dates_for_employee, get_leave_period, set_employee_name, share_doc_with_approver, @@ -159,33 +160,57 @@ class LeaveApplication(Document): .format(formatdate(future_allocation[0].from_date), future_allocation[0].name)) def update_attendance(self): - if self.status == "Approved": - for dt in daterange(getdate(self.from_date), getdate(self.to_date)): - date = dt.strftime("%Y-%m-%d") - status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave" - attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee, - attendance_date = date, docstatus = ('!=', 2))) + if self.status != "Approved": + return + holiday_dates = [] + if not frappe.db.get_value("Leave Type", self.leave_type, "include_holiday"): + holiday_dates = get_holiday_dates_for_employee(self.employee, self.from_date, self.to_date) + + for dt in daterange(getdate(self.from_date), getdate(self.to_date)): + date = dt.strftime("%Y-%m-%d") + attendance_name = frappe.db.exists("Attendance", dict(employee = self.employee, + attendance_date = date, docstatus = ('!=', 2))) + + # don't mark attendance for holidays + # if leave type does not include holidays within leaves as leaves + if date in holiday_dates: if attendance_name: - # update existing attendance, change absent to on leave - doc = frappe.get_doc('Attendance', attendance_name) - if doc.status != status: - doc.db_set('status', status) - doc.db_set('leave_type', self.leave_type) - doc.db_set('leave_application', self.name) - else: - # make new attendance and submit it - doc = frappe.new_doc("Attendance") - doc.employee = self.employee - doc.employee_name = self.employee_name - doc.attendance_date = date - doc.company = self.company - doc.leave_type = self.leave_type - doc.leave_application = self.name - doc.status = status - doc.flags.ignore_validate = True - doc.insert(ignore_permissions=True) - doc.submit() + # cancel and delete existing attendance for holidays + attendance = frappe.get_doc("Attendance", attendance_name) + attendance.flags.ignore_permissions = True + if attendance.docstatus == 1: + attendance.cancel() + frappe.delete_doc("Attendance", attendance_name, force=1) + continue + + self.create_or_update_attendance(attendance_name, date) + + def create_or_update_attendance(self, attendance_name, date): + status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave" + + if attendance_name: + # update existing attendance, change absent to on leave + doc = frappe.get_doc('Attendance', attendance_name) + if doc.status != status: + doc.db_set({ + 'status': status, + 'leave_type': self.leave_type, + 'leave_application': self.name + }) + else: + # make new attendance and submit it + doc = frappe.new_doc("Attendance") + doc.employee = self.employee + doc.employee_name = self.employee_name + doc.attendance_date = date + doc.company = self.company + doc.leave_type = self.leave_type + doc.leave_application = self.name + doc.status = status + doc.flags.ignore_validate = True + doc.insert(ignore_permissions=True) + doc.submit() def cancel_attendance(self): if self.docstatus == 2: diff --git a/erpnext/hr/doctype/leave_application/leave_application_email_template.html b/erpnext/hr/doctype/leave_application/leave_application_email_template.html index 14ca41bebca..dae9084f791 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_email_template.html +++ b/erpnext/hr/doctype/leave_application/leave_application_email_template.html @@ -23,3 +23,8 @@ {{status}} + + {% set doc_link = frappe.utils.get_url_to_form('Leave Application', name) %} + +

+ {{ _('Open Now') }} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index f73d3e52da1..75e99f8991e 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -5,7 +5,16 @@ import unittest import frappe from frappe.permissions import clear_user_permissions_for_doctype -from frappe.utils import add_days, add_months, getdate, nowdate +from frappe.utils import ( + add_days, + add_months, + get_first_day, + get_last_day, + get_year_ending, + get_year_start, + getdate, + nowdate, +) from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation @@ -19,6 +28,10 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( create_assignment_for_multiple_employees, ) from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"] @@ -61,13 +74,15 @@ class TestLeaveApplication(unittest.TestCase): for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]: frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec + frappe.set_user("Administrator") + @classmethod def setUpClass(cls): set_leave_approver() frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'") def tearDown(self): - frappe.set_user("Administrator") + frappe.db.rollback() def _clear_roles(self): frappe.db.sql("""delete from `tabHas Role` where parent in @@ -106,6 +121,72 @@ class TestLeaveApplication(unittest.TestCase): for d in ('2018-01-01', '2018-01-02', '2018-01-03'): self.assertTrue(getdate(d) in dates) + def test_attendance_for_include_holidays(self): + # Case 1: leave type with 'Include holidays within leaves as leaves' enabled + frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Include Holidays", + doctype="Leave Type", + include_holiday=True + )).insert() + + date = getdate() + make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + + holiday_list = make_holiday_list() + frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list) + first_sunday = get_first_sunday(holiday_list) + + leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application.reload() + self.assertEqual(leave_application.total_leave_days, 4) + self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4) + + leave_application.cancel() + + def test_attendance_update_for_exclude_holidays(self): + # Case 2: leave type with 'Include holidays within leaves as leaves' disabled + frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Do Not Include Holidays", + doctype="Leave Type", + include_holiday=False + )).insert() + + date = getdate() + make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + + holiday_list = make_holiday_list() + frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list) + first_sunday = get_first_sunday(holiday_list) + + # already marked attendance on a holiday should be deleted in this case + config = { + "doctype": "Attendance", + "employee": "_T-Employee-00001", + "status": "Present" + } + attendance_on_holiday = frappe.get_doc(config) + attendance_on_holiday.attendance_date = first_sunday + attendance_on_holiday.save() + + # already marked attendance on a non-holiday should be updated + attendance = frappe.get_doc(config) + attendance.attendance_date = add_days(first_sunday, 3) + attendance.save() + + leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application.reload() + # holiday should be excluded while marking attendance + self.assertEqual(leave_application.total_leave_days, 3) + self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3) + + # attendance on holiday deleted + self.assertFalse(frappe.db.exists("Attendance", attendance_on_holiday.name)) + + # attendance on non-holiday updated + self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave") + def test_block_list(self): self._clear_roles() @@ -241,7 +322,13 @@ class TestLeaveApplication(unittest.TestCase): leave_period = get_leave_period() today = nowdate() holiday_list = 'Test Holiday List for Optional Holiday' - optional_leave_date = add_days(today, 7) + employee = get_employee() + + default_holiday_list = make_holiday_list() + frappe.db.set_value("Company", "_Test Company", "default_holiday_list", default_holiday_list) + first_sunday = get_first_sunday(default_holiday_list) + + optional_leave_date = add_days(first_sunday, 1) if not frappe.db.exists('Holiday List', holiday_list): frappe.get_doc(dict( @@ -253,7 +340,6 @@ class TestLeaveApplication(unittest.TestCase): dict(holiday_date = optional_leave_date, description = 'Test') ] )).insert() - employee = get_employee() frappe.db.set_value('Leave Period', leave_period.name, 'optional_holiday_list', holiday_list) leave_type = 'Test Optional Type' @@ -266,7 +352,7 @@ class TestLeaveApplication(unittest.TestCase): allocate_leaves(employee, leave_period, leave_type, 10) - date = add_days(today, 6) + date = add_days(first_sunday, 2) leave_application = frappe.get_doc(dict( doctype = 'Leave Application', @@ -443,6 +529,7 @@ class TestLeaveApplication(unittest.TestCase): leave_policy = frappe.get_doc({ "doctype": "Leave Policy", + "title": "Test Leave Policy", "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}] }).insert() @@ -636,13 +723,13 @@ def create_carry_forwarded_allocation(employee, leave_type): carry_forward=1) leave_allocation.submit() -def make_allocation_record(employee=None, leave_type=None): +def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None): allocation = frappe.get_doc({ "doctype": "Leave Allocation", "employee": employee or "_T-Employee-00001", "leave_type": leave_type or "_Test Leave Type", - "from_date": "2013-01-01", - "to_date": "2019-12-31", + "from_date": from_date or "2013-01-01", + "to_date": to_date or "2019-12-31", "new_leaves_allocated": 30 }) @@ -691,3 +778,16 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el }).insert() allocate_leave.submit() + + +def get_first_sunday(holiday_list): + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = %s + and holiday_date between %s and %s + order by holiday_date + """, (holiday_list, month_start_date, month_end_date))[0][0] + + return first_sunday \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.json b/erpnext/hr/doctype/leave_encashment/leave_encashment.json index 1f6c03f7b60..cc4e53eb902 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.json +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.json @@ -154,10 +154,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:32:55.492327", + "modified": "2022-01-18 19:16:52.414356", "modified_by": "Administrator", "module": "HR", "name": "Leave Encashment", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -218,7 +219,10 @@ "write": 1 } ], + "search_fields": "employee,employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_period/leave_period.json b/erpnext/hr/doctype/leave_period/leave_period.json index 9e895c34fb2..84ce1147e9a 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.json +++ b/erpnext/hr/doctype/leave_period/leave_period.json @@ -1,294 +1,108 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "HR-LPR-.YYYY.-.#####", - "beta": 0, "creation": "2018-04-13 15:20:52.864288", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "from_date", + "to_date", + "is_active", + "column_break_3", + "company", + "optional_holiday_list" + ], "fields": [ { - "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": 1, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "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": 1, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "is_active", "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": "Is Active", - "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 + "label": "Is Active" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_3", - "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 + "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": 1, - "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": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "optional_holiday_list", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Holiday List for Optional Leave", - "length": 0, - "no_copy": 0, - "options": "Holiday List", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Holiday List" } ], - "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": "2019-05-30 16:15:43.305502", + "links": [], + "modified": "2022-01-13 13:28:12.951025", "modified_by": "Administrator", "module": "HR", "name": "Leave Period", - "name_case": "", + "naming_rule": "Expression (old style)", "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": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "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": "HR Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "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": "HR User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, + "search_fields": "from_date, to_date, company", "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy/leave_policy.json b/erpnext/hr/doctype/leave_policy/leave_policy.json index 373095d075f..6ac8f20ea2d 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy.json +++ b/erpnext/hr/doctype/leave_policy/leave_policy.json @@ -1,131 +1,55 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "HR-LPOL-.YYYY.-.#####", - "beta": 0, "creation": "2018-04-13 16:06:19.507624", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "title", + "leave_allocations_section", + "leave_policy_details", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, "allow_in_quick_entry": 1, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "leave_allocations_section", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Leave Allocations", - "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 + "label": "Leave Allocations" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "leave_policy_details", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Leave Policy Details", - "length": 0, - "no_copy": 0, "options": "Leave Policy Detail", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "amended_from", "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": "Amended From", - "length": 0, "no_copy": 1, "options": "Leave Policy", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-29 08:42:53.363088", + "links": [], + "modified": "2022-01-19 13:07:40.556500", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy", - "name_case": "", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -135,14 +59,10 @@ "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 @@ -154,14 +74,10 @@ "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 @@ -173,26 +89,19 @@ "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR User", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, + "search_fields": "title", "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "states": [], + "title_field": "title", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy/test_leave_policy.py b/erpnext/hr/doctype/leave_policy/test_leave_policy.py index 3dbbef857ec..a4b8af759e5 100644 --- a/erpnext/hr/doctype/leave_policy/test_leave_policy.py +++ b/erpnext/hr/doctype/leave_policy/test_leave_policy.py @@ -24,6 +24,7 @@ def create_leave_policy(**args): args = frappe._dict(args) return frappe.get_doc({ "doctype": "Leave Policy", + "title": "Test Leave Policy", "leave_policy_details": [{ "leave_type": args.leave_type or "_Test Leave Type", "annual_allocation": args.annual_allocation or 10 diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json index 3373350e733..27f0540b247 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -113,10 +113,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-01 17:54:01.014509", + "modified": "2022-01-13 13:37:11.218882", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy Assignment", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -164,5 +165,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js index 8b954c46a10..6b75817cba9 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js @@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = { if (cur_dialog.fields_dict.leave_period.value) { me.set_effective_date(); } - } + }, + get_query() { + let filters = {"is_active": 1}; + if (cur_dialog.fields_dict.company.value) + filters["company"] = cur_dialog.fields_dict.company.value; + + return { + filters: filters + }; + }, }, { fieldtype: "Column Break" diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 8953a51e8bb..3b7f8ec822c 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -108,6 +108,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): leave_policy = frappe.get_doc({ "doctype": "Leave Policy", + "title": "Test Leave Policy", "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}] }).insert() diff --git a/erpnext/hr/doctype/training_feedback/training_feedback.json b/erpnext/hr/doctype/training_feedback/training_feedback.json index cd967d514f4..ebf5a506f03 100644 --- a/erpnext/hr/doctype/training_feedback/training_feedback.json +++ b/erpnext/hr/doctype/training_feedback/training_feedback.json @@ -1,443 +1,144 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "HR-TRF-.YYYY.-.#####", - "beta": 0, - "creation": "2016-08-08 06:35:34.158568", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "autoname": "HR-TRF-.YYYY.-.#####", + "creation": "2016-08-08 06:35:34.158568", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "department", + "course", + "column_break_3", + "training_event", + "event_name", + "trainer_name", + "section_break_6", + "feedback", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "employee", + "fieldtype": "Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Read Only", + "in_global_search": 1, + "label": "Employee Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "training_event.course", - "fieldname": "course", - "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": "Course", - "length": 0, - "no_copy": 0, - "options": "Course", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "training_event.course", + "fieldname": "course", + "fieldtype": "Link", + "label": "Course", + "options": "Course", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "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_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "training_event", - "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": 1, - "label": "Training Event", - "length": 0, - "no_copy": 0, - "options": "Training Event", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "training_event", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Training Event", + "options": "Training Event", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "training_event.event_name", - "fieldname": "event_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Event Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "training_event.event_name", + "fieldname": "event_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Event Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "training_event.trainer_name", - "fieldname": "trainer_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Trainer Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "training_event.trainer_name", + "fieldname": "trainer_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Trainer Name", + "read_only": 1 + }, { - "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, - "fieldname": "feedback", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Feedback", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "feedback", + "fieldtype": "Text", + "label": "Feedback", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Training Feedback", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Training Feedback", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-30 11:28:13.849860", - "modified_by": "Administrator", - "module": "HR", - "name": "Training Feedback", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2022-01-18 19:32:20.805277", + "modified_by": "Administrator", + "module": "HR", + "name": "Training Feedback", + "naming_rule": "Expression (old style)", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "employee_name", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "search_fields": "employee_name, training_event, event_name", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "employee_name" } \ No newline at end of file diff --git a/erpnext/hr/doctype/training_result/training_result.json b/erpnext/hr/doctype/training_result/training_result.json index dd7abd7753f..f28669e3c22 100644 --- a/erpnext/hr/doctype/training_result/training_result.json +++ b/erpnext/hr/doctype/training_result/training_result.json @@ -1,226 +1,83 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "HR-TRR-.YYYY.-.#####", - "beta": 0, - "creation": "2016-11-04 02:13:48.407576", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "autoname": "HR-TRR-.YYYY.-.#####", + "creation": "2016-11-04 02:13:48.407576", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "training_event", + "section_break_3", + "employees", + "amended_from", + "employee_emails" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "training_event", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Training Event", - "length": 0, - "no_copy": 0, - "options": "Training Event", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "training_event", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Training Event", + "options": "Training Event", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_3", - "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_3", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employees", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employees", - "length": 0, - "no_copy": 0, - "options": "Training Result Employee", - "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": "employees", + "fieldtype": "Table", + "label": "Employees", + "options": "Training Result Employee" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Training Result", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Training Result", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee_emails", - "fieldtype": "Small Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Emails", - "length": 0, - "no_copy": 0, - "options": "Email", - "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": "employee_emails", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Employee Emails", + "options": "Email" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:47.614563", - "modified_by": "Administrator", - "module": "HR", - "name": "Training Result", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2022-01-18 19:31:44.900034", + "modified_by": "Administrator", + "module": "HR", + "name": "Training Result", + "naming_rule": "Expression (old style)", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "training_event", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "search_fields": "training_event", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "training_event" } \ No newline at end of file diff --git a/erpnext/hr/doctype/travel_request/travel_request.json b/erpnext/hr/doctype/travel_request/travel_request.json index 441907c02d1..7908e1a8ea7 100644 --- a/erpnext/hr/doctype/travel_request/travel_request.json +++ b/erpnext/hr/doctype/travel_request/travel_request.json @@ -216,10 +216,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2019-12-12 18:42:26.451359", + "modified": "2022-01-18 19:19:33.678664", "modified_by": "Administrator", "module": "HR", "name": "Travel Request", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -235,7 +236,10 @@ "write": 1 } ], + "search_fields": "employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index 85e641c856c..30cec1b4a8b 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -5,7 +5,7 @@ "label": "Outgoing Salary" } ], - "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-03-02 15:48:58.322521", "docstatus": 0, "doctype": "Workspace", @@ -1642,7 +1642,7 @@ "type": "Link" } ], - "modified": "2021-12-05 22:05:13.004462", + "modified": "2022-01-13 17:38:45.489128", "modified_by": "Administrator", "module": "HR", "name": "HR", @@ -1651,7 +1651,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 14, + "sequence_id": 14.0, "shortcuts": [ { "color": "Green", diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json index 7deee0d4612..b08a85e213e 100644 --- a/erpnext/loan_management/workspace/loan_management/loan_management.json +++ b/erpnext/loan_management/workspace/loan_management/loan_management.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Loan Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Loan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Processes\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Disbursement and Repayment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Security\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan Application\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan Processes\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Disbursement and Repayment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan Security\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "creation": "2020-03-12 16:35:55.299820", "docstatus": 0, "doctype": "Workspace", @@ -238,7 +238,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:18:13.350905", + "modified": "2022-01-13 17:39:16.790152", "modified_by": "Administrator", "module": "Loan Management", "name": "Loans", @@ -247,7 +247,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 16, + "sequence_id": 16.0, "shortcuts": [ { "color": "Green", diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 2ffae1a4f2a..07d928c221f 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt - import frappe from frappe import _, throw from frappe.utils import add_days, cint, cstr, date_diff, formatdate, getdate @@ -306,13 +305,18 @@ class MaintenanceSchedule(TransactionBase): return schedule.name @frappe.whitelist() -def update_serial_nos(s_id): - serial_nos = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'serial_no') +def get_serial_nos_from_schedule(item_code, schedule=None): + serial_nos = [] + if schedule: + serial_nos = frappe.db.get_value('Maintenance Schedule Item', { + 'parent': schedule, + 'item_code': item_code + }, 'serial_no') + if serial_nos: serial_nos = get_serial_nos(serial_nos) - return serial_nos - else: - return False + + return serial_nos @frappe.whitelist() def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None): @@ -320,12 +324,9 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No def update_status_and_detail(source, target, parent): target.maintenance_type = "Scheduled" - target.maintenance_schedule = source.name target.maintenance_schedule_detail = s_id - def update_sales_and_serial(source, target, parent): - sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person') - target.service_person = sales_person + def update_serial(source, target, parent): serial_nos = get_serial_nos(target.serial_no) if len(serial_nos) == 1: target.serial_no = serial_nos[0] @@ -346,7 +347,10 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No "Maintenance Schedule Item": { "doctype": "Maintenance Visit Purpose", "condition": lambda doc: doc.item_name == item_name, - "postprocess": update_sales_and_serial + "field_map": { + "sales_person": "service_person" + }, + "postprocess": update_serial } }, target_doc) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index 501712613a8..6e727e53efd 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -4,11 +4,15 @@ import unittest import frappe +from frappe.utils import format_date from frappe.utils.data import add_days, formatdate, today from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import ( + get_serial_nos_from_schedule, make_maintenance_visit, ) +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item # test_records = frappe.get_test_records('Maintenance Schedule') @@ -79,6 +83,49 @@ class TestMaintenanceSchedule(unittest.TestCase): #checks if visit status is back updated in schedule self.assertTrue(ms.schedules[1].completion_status, "Partially Completed") + self.assertEqual(format_date(visit.mntc_date), format_date(ms.schedules[1].actual_date)) + + #checks if visit status is updated on cancel + visit.cancel() + ms.reload() + self.assertTrue(ms.schedules[1].completion_status, "Pending") + self.assertEqual(ms.schedules[1].actual_date, None) + + def test_serial_no_filters(self): + # Without serial no. set in schedule -> returns None + item_code = "_Test Serial Item" + make_serial_item_with_serial(item_code) + ms = make_maintenance_schedule(item_code=item_code) + ms.submit() + + s_item = ms.schedules[0] + mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name) + mvi = mv.purposes[0] + serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name) + self.assertEqual(serial_nos, None) + + # With serial no. set in schedule -> returns serial nos. + make_serial_item_with_serial(item_code) + ms = make_maintenance_schedule(item_code=item_code, serial_no="TEST001, TEST002") + ms.submit() + + s_item = ms.schedules[0] + mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name) + mvi = mv.purposes[0] + serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name) + self.assertEqual(serial_nos, ["TEST001", "TEST002"]) + + frappe.db.rollback() + +def make_serial_item_with_serial(item_code): + serial_item_doc = create_item(item_code, is_stock_item=1) + if not serial_item_doc.has_serial_no or not serial_item_doc.serial_no_series: + serial_item_doc.has_serial_no = 1 + serial_item_doc.serial_no_series = "TEST.###" + serial_item_doc.save(ignore_permissions=True) + active_serials = frappe.db.get_all('Serial No', {"status": "Active", "item_code": item_code}) + if len(active_serials) < 2: + make_serialized_item(item_code=item_code) def get_events(ms): return frappe.get_all("Event Participants", filters={ @@ -87,17 +134,18 @@ def get_events(ms): "parenttype": "Event" }) -def make_maintenance_schedule(): +def make_maintenance_schedule(**args): ms = frappe.new_doc("Maintenance Schedule") ms.company = "_Test Company" ms.customer = "_Test Customer" ms.transaction_date = today() ms.append("items", { - "item_code": "_Test Item", + "item_code": args.get("item_code") or "_Test Item", "start_date": today(), "periodicity": "Weekly", "no_of_visits": 4, + "serial_no": args.get("serial_no"), "sales_person": "Sales Team", }) ms.insert(ignore_permissions=True) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index d2197a6877d..72686e7403f 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -2,52 +2,54 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.maintenance"); -var serial_nos = []; frappe.ui.form.on('Maintenance Visit', { - refresh: function (frm) { - //filters for serial_no based on item_code - frm.set_query('serial_no', 'purposes', function (frm, cdt, cdn) { - let item = locals[cdt][cdn]; - if (serial_nos) { - return { - filters: { - 'item_code': item.item_code, - 'name': ["in", serial_nos] - } - }; - } else { - return { - filters: { - 'item_code': item.item_code - } - }; - } - }); - }, setup: function (frm) { frm.set_query('contact_person', erpnext.queries.contact_query); frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer', erpnext.queries.customer); }, - onload: function (frm, cdt, cdn) { - let item = locals[cdt][cdn]; + onload: function (frm) { + // filters for serial no based on item code if (frm.doc.maintenance_type === "Scheduled") { - const schedule_id = item.purposes[0].prevdoc_detail_docname || frm.doc.maintenance_schedule_detail; + let item_code = frm.doc.purposes[0].item_code; frappe.call({ - method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos", + method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule", args: { - s_id: schedule_id - }, - callback: function (r) { - serial_nos = r.message; + schedule: frm.doc.maintenance_schedule, + item_code: item_code } + }).then((r) => { + let serial_nos = r.message; + frm.set_query('serial_no', 'purposes', () => { + if (serial_nos.length > 0) { + return { + filters: { + 'item_code': item_code, + 'name': ["in", serial_nos] + } + }; + } + return { + filters: { + 'item_code': item_code + } + }; + }); + }); + } else { + frm.set_query('serial_no', 'purposes', (frm, cdt, cdn) => { + let row = locals[cdt][cdn]; + return { + filters: { + 'item_code': row.item_code + } + }; }); } if (!frm.doc.status) { frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { - frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes"); frm.set_value({ mntc_date: frappe.datetime.get_today() }); } }, @@ -60,7 +62,6 @@ frappe.ui.form.on('Maintenance Visit', { contact_person: function (frm) { erpnext.utils.get_contact_details(frm); } - }) // TODO commonify this code diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json index ec32239518f..4a6aa0a34bf 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json @@ -179,8 +179,7 @@ "label": "Purposes", "oldfieldname": "maintenance_visit_details", "oldfieldtype": "Table", - "options": "Maintenance Visit Purpose", - "reqd": 1 + "options": "Maintenance Visit Purpose" }, { "fieldname": "more_info", @@ -294,10 +293,11 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-27 16:06:17.352572", + "modified": "2021-12-17 03:10:27.608112", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py index 5a87b162af6..6fe2466be22 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import get_datetime +from frappe.utils import format_date, get_datetime from erpnext.utilities.transaction_base import TransactionBase @@ -18,25 +18,34 @@ class MaintenanceVisit(TransactionBase): if d.serial_no and not frappe.db.exists("Serial No", d.serial_no): frappe.throw(_("Serial No {0} does not exist").format(d.serial_no)) + def validate_purpose_table(self): + if not self.purposes: + frappe.throw(_("Add Items in the Purpose Table"), title="Purposes Required") + def validate_maintenance_date(self): if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail: item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference') if item_ref: start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date']) if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date): - frappe.throw(_("Date must be between {0} and {1}").format(start_date, end_date)) + frappe.throw(_("Date must be between {0} and {1}") + .format(format_date(start_date), format_date(end_date))) + def validate(self): self.validate_serial_no() self.validate_maintenance_date() + self.validate_purpose_table() - def update_completion_status(self): + def update_status_and_actual_date(self, cancel=False): + status = "Pending" + actual_date = None + if not cancel: + status = self.completion_status + actual_date = self.mntc_date if self.maintenance_schedule_detail: - frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', self.completion_status) - - def update_actual_date(self): - if self.maintenance_schedule_detail: - frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', self.mntc_date) + frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', status) + frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', actual_date) def update_customer_issue(self, flag): if not self.maintenance_schedule: @@ -97,12 +106,12 @@ class MaintenanceVisit(TransactionBase): def on_submit(self): self.update_customer_issue(1) frappe.db.set(self, 'status', 'Submitted') - self.update_completion_status() - self.update_actual_date() + self.update_status_and_actual_date() def on_cancel(self): self.check_if_last_visit() frappe.db.set(self, 'status', 'Cancelled') + self.update_status_and_actual_date(cancel=True) def on_update(self): pass diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 6d35d65bea9..fc3b971bcb9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -331,7 +331,7 @@ frappe.ui.form.on("BOM", { }); }); - if (has_template_rm) { + if (has_template_rm && has_template_rm.length) { dialog.fields_dict.items.grid.refresh(); } }, @@ -467,7 +467,8 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { "uom": d.uom, "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor, - "sourced_by_supplier": d.sourced_by_supplier + "sourced_by_supplier": d.sourced_by_supplier, + "do_not_explode": d.do_not_explode }, callback: function(r) { d = locals[cdt][cdn]; @@ -640,6 +641,13 @@ frappe.ui.form.on("BOM Operation", "workstation", function(frm, cdt, cdn) { }); }); +frappe.ui.form.on("BOM Item", { + do_not_explode: function(frm, cdt, cdn) { + get_bom_material_detail(frm.doc, cdt, cdn, false); + } +}) + + frappe.ui.form.on("BOM Item", "qty", function(frm, cdt, cdn) { var d = locals[cdt][cdn]; d.stock_qty = d.qty * d.conversion_factor; diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 218ac64d8da..0b441969400 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -37,7 +37,6 @@ "inspection_required", "quality_inspection_template", "column_break_31", - "bom_level", "section_break_33", "items", "scrap_section", @@ -522,13 +521,6 @@ "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", @@ -540,7 +532,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-11-18 13:04:16.271975", + "modified": "2022-01-30 21:27:54.727298", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", @@ -577,5 +569,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 5a60fb751d1..d640f3fda7e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -149,12 +149,12 @@ class BOM(WebsiteGenerator): self.set_bom_material_details() self.set_bom_scrap_items_detail() self.validate_materials() + self.validate_transfer_against() self.set_routing_operations() self.validate_operations() self.calculate_cost() self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) - self.set_bom_level() self.validate_scrap_items() def get_context(self, context): @@ -203,6 +203,10 @@ class BOM(WebsiteGenerator): for item in self.get("items"): self.validate_bom_currency(item) + item.bom_no = '' + if not item.do_not_explode: + item.bom_no = item.bom_no + ret = self.get_bom_material_detail({ "company": self.company, "item_code": item.item_code, @@ -214,8 +218,10 @@ class BOM(WebsiteGenerator): "uom": item.uom, "stock_uom": item.stock_uom, "conversion_factor": item.conversion_factor, - "sourced_by_supplier": item.sourced_by_supplier + "sourced_by_supplier": item.sourced_by_supplier, + "do_not_explode": item.do_not_explode }) + for r in ret: if not item.get(r): item.set(r, ret[r]) @@ -267,6 +273,9 @@ class BOM(WebsiteGenerator): 'sourced_by_supplier' : args.get('sourced_by_supplier', 0) } + if args.get('do_not_explode'): + ret_item['bom_no'] = '' + return ret_item def validate_bom_currency(self, item): @@ -681,6 +690,12 @@ class BOM(WebsiteGenerator): if act_pbom and act_pbom[0][0]: frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs")) + def validate_transfer_against(self): + if not self.with_operations: + self.transfer_material_against = "Work Order" + if not self.transfer_material_against and not self.is_new(): + frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value")) + def set_routing_operations(self): if self.routing and self.with_operations and not self.operations: self.get_routing() @@ -700,20 +715,6 @@ class BOM(WebsiteGenerator): """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 validate_scrap_items(self): for item in self.scrap_items: msg = "" diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 178d92c26c3..53437c80127 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -385,6 +385,53 @@ class TestBOM(ERPNextTestCase): self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results") self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results") + def test_exclude_exploded_items_from_bom(self): + bom_no = get_default_bom() + new_bom = frappe.copy_doc(frappe.get_doc('BOM', bom_no)) + for row in new_bom.items: + if row.item_code == '_Test Item Home Desktop Manufactured': + self.assertTrue(row.bom_no) + row.do_not_explode = True + + new_bom.docstatus = 0 + new_bom.save() + new_bom.load_from_db() + + for row in new_bom.items: + if row.item_code == '_Test Item Home Desktop Manufactured' and row.do_not_explode: + self.assertFalse(row.bom_no) + + new_bom.delete() + + def test_valid_transfer_defaults(self): + bom_with_op = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1}) + bom = frappe.copy_doc(frappe.get_doc("BOM", bom_with_op), ignore_no_copy=False) + + # test defaults + bom.docstatus = 0 + bom.transfer_material_against = None + bom.insert() + self.assertEqual(bom.transfer_material_against, "Work Order") + + bom.reload() + bom.transfer_material_against = None + with self.assertRaises(frappe.ValidationError): + bom.save() + bom.reload() + + # test saner default + bom.transfer_material_against = "Job Card" + bom.with_operations = 0 + bom.save() + self.assertEqual(bom.transfer_material_against, "Work Order") + + # test no value on existing doc + bom.transfer_material_against = None + bom.with_operations = 0 + bom.save() + self.assertEqual(bom.transfer_material_against, "Work Order") + bom.delete() + 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}) diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index 4c9877f52b2..3406215cbbb 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -10,6 +10,7 @@ "item_name", "operation", "column_break_3", + "do_not_explode", "bom_no", "source_warehouse", "allow_alternative_item", @@ -73,6 +74,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.do_not_explode", "fieldname": "bom_no", "fieldtype": "Link", "in_filter": 1, @@ -284,18 +286,25 @@ "fieldname": "sourced_by_supplier", "fieldtype": "Check", "label": "Sourced by Supplier" + }, + { + "default": "0", + "fieldname": "do_not_explode", + "fieldtype": "Check", + "label": "Do Not Explode" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-08 14:19:37.563300", + "modified": "2022-01-24 16:57:57.020232", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 7cec7f515a3..4290ca3e4c8 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -559,9 +559,11 @@ class ProductionPlan(Document): 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) - 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) + self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True) + for idx, row in enumerate(self.sub_assembly_items, start=1): + row.idx = idx + def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): for data in bom_data: data.qty = data.stock_qty data.production_plan_item = row.name @@ -947,11 +949,8 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): locations = get_available_item_locations(item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True) - if not locations: - new_mr_items.append(item) - return - required_qty = item.get("quantity") + # get available material by transferring to production warehouse for d in locations: if required_qty <=0: return @@ -962,14 +961,34 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): new_dict.update({ "quantity": quantity, "material_request_type": "Material Transfer", + "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM "from_warehouse": d.get("warehouse") }) required_qty -= quantity new_mr_items.append(new_dict) + # raise purchase request for remaining qty if required_qty: + stock_uom, purchase_uom = frappe.db.get_value( + 'Item', + item['item_code'], + ['stock_uom', 'purchase_uom'] + ) + + if purchase_uom != stock_uom and purchase_uom == item['uom']: + conversion_factor = get_uom_conversion_factor(item['item_code'], item['uom']) + if not (conversion_factor or frappe.flags.show_qty_in_stock_uom): + frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}") + .format(purchase_uom, stock_uom, item['item_code'])) + + required_qty = required_qty / conversion_factor + + if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): + required_qty = ceil(required_qty) + item["quantity"] = required_qty + new_mr_items.append(item) @frappe.whitelist() @@ -987,9 +1006,6 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): for d in data: if d.expandable: 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) - stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) bom_data.append(frappe._dict({ 'parent_item_code': parent_item_code, @@ -1000,7 +1016,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): 'uom': d.stock_uom, 'bom_no': d.value, 'is_sub_contracted_item': d.is_sub_contracted_item, - 'bom_level': bom_level, + 'bom_level': indent, 'indent': indent, 'stock_qty': stock_qty })) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2febc1e23c0..21a126b2a79 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -347,6 +347,45 @@ class TestProductionPlan(ERPNextTestCase): frappe.db.rollback() + def test_subassmebly_sorting(self): + """ Test subassembly sorting in case of multiple items with nested BOMs""" + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + prefix = "_TestLevel_" + boms = { + "Assembly": { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly2": {"ChildPart3": {}}, + "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, + "ChildPart5": {}, + "ChildPart6": {}, + "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, + }, + "MegaDeepAssy": { + "SecretSubassy": {"SecretPart": {"VerySecret" : { "SuperSecret": {"Classified": {}}}},}, + # ^ assert that this is + # first item in subassy table + } + } + create_nested_bom(boms, prefix=prefix) + + items = [prefix + item_code for item_code in boms.keys()] + plan = create_production_plan(item_code=items[0], do_not_save=True) + plan.append("po_items", { + 'use_multi_level_bom': 1, + 'item_code': items[1], + 'bom_no': frappe.db.get_value('Item', items[1], 'default_bom'), + 'planned_qty': 1, + 'planned_start_date': now_datetime() + }) + plan.get_sub_assembly_items() + + bom_level_order = [d.bom_level for d in plan.sub_assembly_items] + self.assertEqual(bom_level_order, sorted(bom_level_order, reverse=True)) + # lowest most level of subassembly should be first + self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item) + + def create_production_plan(**args): args = frappe._dict(args) 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 index 657ee35a852..45ea26c3a8a 100644 --- 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 @@ -102,7 +102,6 @@ }, { "columns": 1, - "fetch_from": "bom_no.bom_level", "fieldname": "bom_level", "fieldtype": "Int", "in_list_view": 1, @@ -189,7 +188,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-28 20:10:56.296410", + "modified": "2022-01-30 21:31:10.527559", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", @@ -198,5 +197,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 9926b15894c..a399edda707 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe -from frappe.utils import add_months, cint, flt, now, today +from frappe.utils import add_days, add_months, cint, flt, now, today from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom @@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import ( OverProductionError, StockOverProductionError, close_work_order, + make_job_card, make_stock_entry, stop_unstop, ) @@ -804,6 +805,34 @@ class TestWorkOrder(ERPNextTestCase): if row.is_scrap_item: self.assertEqual(row.qty, 1) + # Partial Job Card 1 with qty 10 + wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1) + job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + update_job_card(job_card, 10) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + for row in stock_entry.items: + if row.is_scrap_item: + self.assertEqual(row.qty, 2) + + # Partial Job Card 2 with qty 10 + operations = [] + wo_order.load_from_db() + for row in wo_order.operations: + n_dict = row.as_dict() + n_dict['qty'] = 10 + n_dict['pending_qty'] = 10 + operations.append(n_dict) + + make_job_card(wo_order.name, operations) + job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name') + update_job_card(job_card, 10) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + for row in stock_entry.items: + if row.is_scrap_item: + self.assertEqual(row.qty, 2) + def test_close_work_order(self): items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO', 'Test RM Item 2 for Closed WO'] @@ -882,8 +911,57 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins) + def test_partial_manufacture_entries(self): + cancel_stock_entry = [] -def update_job_card(job_card): + frappe.db.set_value("Manufacturing Settings", None, + "backflush_raw_materials_based_on", "Material Transferred for Manufacture") + + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100) + ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", + target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0) + ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0) + + cancel_stock_entry.extend([ste1.name, ste2.name]) + + sm = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 100)) + for row in sm.get('items'): + if row.get('item_code') == '_Test Item': + row.qty = 110 + + sm.submit() + cancel_stock_entry.append(sm.name) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 90)) + for row in s.get('items'): + if row.get('item_code') == '_Test Item': + self.assertEqual(row.get('qty'), 100) + s.submit() + cancel_stock_entry.append(s.name) + + s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5)) + for row in s1.get('items'): + if row.get('item_code') == '_Test Item': + self.assertEqual(row.get('qty'), 5) + s1.submit() + cancel_stock_entry.append(s1.name) + + s2 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5)) + for row in s2.get('items'): + if row.get('item_code') == '_Test Item': + self.assertEqual(row.get('qty'), 5) + + cancel_stock_entry.reverse() + for ste in cancel_stock_entry: + doc = frappe.get_doc("Stock Entry", ste) + doc.cancel() + + frappe.db.set_value("Manufacturing Settings", None, + "backflush_raw_materials_based_on", "BOM") + +def update_job_card(job_card, jc_qty=None): + employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name') job_card_doc = frappe.get_doc('Job Card', job_card) job_card_doc.set('scrap_items', [ { @@ -896,8 +974,12 @@ def update_job_card(job_card): }, ]) + if jc_qty: + job_card_doc.for_quantity = jc_qty + job_card_doc.append('time_logs', { 'from_time': now(), + 'employee': employee, 'time_in_mins': 60, 'completed_qty': job_card_doc.for_quantity }) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 5ffbb0374e9..6433a992830 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -131,16 +131,14 @@ frappe.ui.form.on("Work Order", { erpnext.work_order.set_custom_buttons(frm); frm.set_intro(""); - if (frm.doc.docstatus === 0 && !frm.doc.__islocal) { + if (frm.doc.docstatus === 0 && !frm.is_new()) { frm.set_intro(__("Submit this Work Order for further processing.")); + } else { + frm.trigger("show_progress_for_items"); + frm.trigger("show_progress_for_operations"); } if (frm.doc.status != "Closed") { - if (frm.doc.docstatus===1) { - frm.trigger('show_progress_for_items'); - frm.trigger('show_progress_for_operations'); - } - if (frm.doc.docstatus === 1 && frm.doc.operations && frm.doc.operations.length) { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 12cd58f418b..9452a63d70b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -333,12 +333,13 @@ "options": "fa fa-wrench" }, { - "default": "Work Order", "depends_on": "operations", + "fetch_from": "bom_no.transfer_material_against", + "fetch_if_empty": 1, "fieldname": "transfer_material_against", "fieldtype": "Select", "label": "Transfer Material Against", - "options": "Work Order\nJob Card" + "options": "\nWork Order\nJob Card" }, { "fieldname": "operations", @@ -574,7 +575,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-11-08 17:36:07.016300", + "modified": "2022-01-24 21:18:12.160114", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", @@ -607,6 +608,7 @@ ], "sort_field": "modified", "sort_order": "ASC", + "states": [], "title_field": "production_item", "track_changes": 1, "track_seen": 1 diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 170454c8238..03e09103454 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -31,6 +31,7 @@ from erpnext.stock.doctype.batch.batch import make_batch from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life from erpnext.stock.doctype.serial_no.serial_no import ( auto_make_serial_nos, + clean_serial_no_string, get_auto_serial_nos, get_serial_nos, ) @@ -65,6 +66,7 @@ class WorkOrder(Document): self.validate_warehouse_belongs_to_company() self.calculate_operating_cost() self.validate_qty() + self.validate_transfer_against() self.validate_operation_time() self.status = self.get_status() @@ -72,6 +74,7 @@ class WorkOrder(Document): self.set_required_items(reset_only_qty = len(self.get("required_items"))) + def validate_sales_order(self): if self.sales_order: self.check_sales_order_on_hold_or_close() @@ -356,6 +359,7 @@ class WorkOrder(Document): frappe.delete_doc("Batch", row.name) def make_serial_nos(self, args): + self.serial_no = clean_serial_no_string(self.serial_no) 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) @@ -625,6 +629,16 @@ class WorkOrder(Document): if not self.qty > 0: frappe.throw(_("Quantity to Manufacture must be greater than 0.")) + def validate_transfer_against(self): + if not self.docstatus == 1: + # let user configure operations until they're ready to submit + return + if not self.operations: + self.transfer_material_against = "Work Order" + if not self.transfer_material_against: + frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value")) + + def validate_operation_time(self): for d in self.operations: if not d.time_in_mins > 0: diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 25de2e03797..19a80ab4076 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -26,8 +26,7 @@ def get_exploded_items(bom, data, indent=0, qty=1): '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_level': indent, 'bom': item.bom_no, 'qty': item.qty * qty, 'uom': item.uom, @@ -73,7 +72,7 @@ def get_columns(): }, { "label": "BOM Level", - "fieldtype": "Data", + "fieldtype": "Int", "fieldname": "bom_level", "width": 100 }, diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js index 7468e34020c..0eb22a22f73 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js @@ -4,6 +4,39 @@ frappe.query_reports["BOM Operations Time"] = { "filters": [ - + { + "fieldname": "item_code", + "label": __("Item Code"), + "fieldtype": "Link", + "width": "100", + "options": "Item", + "get_query": () =>{ + return { + filters: { "disabled": 0, "is_stock_item": 1 } + } + } + }, + { + "fieldname": "bom_id", + "label": __("BOM ID"), + "fieldtype": "MultiSelectList", + "width": "100", + "options": "BOM", + "get_data": function(txt) { + return frappe.db.get_link_options("BOM", txt); + }, + "get_query": () =>{ + return { + filters: { "docstatus": 1, "is_active": 1, "with_operations": 1 } + } + } + }, + { + "fieldname": "workstation", + "label": __("Workstation"), + "fieldtype": "Link", + "width": "100", + "options": "Workstation" + }, ] }; diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json index 665c5b9f79e..8162017ca81 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json @@ -1,14 +1,16 @@ { - "add_total_row": 0, + "add_total_row": 1, + "columns": [], "creation": "2020-03-03 01:41:20.862521", "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "filters": [], "idx": 0, "is_standard": "Yes", "letter_head": "", - "modified": "2020-03-03 01:41:20.862521", + "modified": "2022-01-20 14:21:47.771591", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operations Time", diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py index e7a818abd5d..eda9eb9d701 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py @@ -12,19 +12,15 @@ def execute(filters=None): return columns, data def get_data(filters): - data = [] + bom_wise_data = {} + bom_data, report_data = [], [] - bom_data = [] - for d in frappe.db.sql(""" - SELECT - bom.name, bom.item, bom.item_name, bom.uom, - bomps.operation, bomps.workstation, bomps.time_in_mins - FROM `tabBOM` bom, `tabBOM Operation` bomps - WHERE - bom.docstatus = 1 and bom.is_active = 1 and bom.name = bomps.parent - """, as_dict=1): + bom_operation_data = get_filtered_data(filters) + + for d in bom_operation_data: row = get_args() if d.name not in bom_data: + bom_wise_data[d.name] = [] bom_data.append(d.name) row.update(d) else: @@ -34,14 +30,49 @@ def get_data(filters): "time_in_mins": d.time_in_mins }) - data.append(row) + # maintain BOM wise data for grouping such as: + # {"BOM A": [{Row1}, {Row2}], "BOM B": ...} + bom_wise_data[d.name].append(row) used_as_subassembly_items = get_bom_count(bom_data) - for d in data: - d.used_as_subassembly_items = used_as_subassembly_items.get(d.name, 0) + for d in bom_wise_data: + for row in bom_wise_data[d]: + row.used_as_subassembly_items = used_as_subassembly_items.get(row.name, 0) + report_data.append(row) - return data + return report_data + +def get_filtered_data(filters): + bom = frappe.qb.DocType("BOM") + bom_ops = frappe.qb.DocType("BOM Operation") + + bom_ops_query = ( + frappe.qb.from_(bom) + .join(bom_ops).on(bom.name == bom_ops.parent) + .select( + bom.name, bom.item, bom.item_name, bom.uom, + bom_ops.operation, bom_ops.workstation, bom_ops.time_in_mins + ).where( + (bom.docstatus == 1) + & (bom.is_active == 1) + ) + ) + + if filters.get("item_code"): + bom_ops_query = bom_ops_query.where(bom.item == filters.get("item_code")) + + if filters.get("bom_id"): + bom_ops_query = bom_ops_query.where(bom.name.isin(filters.get("bom_id"))) + + if filters.get("workstation"): + bom_ops_query = bom_ops_query.where( + bom_ops.workstation == filters.get("workstation") + ) + + bom_operation_data = bom_ops_query.run(as_dict=True) + + return bom_operation_data def get_bom_count(bom_data): data = frappe.get_all("BOM Item", @@ -68,13 +99,13 @@ def get_columns(filters): "options": "BOM", "fieldname": "name", "fieldtype": "Link", - "width": 140 + "width": 220 }, { - "label": _("BOM Item Code"), + "label": _("Item Code"), "options": "Item", "fieldname": "item", "fieldtype": "Link", - "width": 140 + "width": 150 }, { "label": _("Item Name"), "fieldname": "item_name", @@ -85,13 +116,13 @@ def get_columns(filters): "options": "UOM", "fieldname": "uom", "fieldtype": "Link", - "width": 140 + "width": 100 }, { "label": _("Operation"), "options": "Operation", "fieldname": "operation", "fieldtype": "Link", - "width": 120 + "width": 140 }, { "label": _("Workstation"), "options": "Workstation", @@ -101,11 +132,11 @@ def get_columns(filters): }, { "label": _("Time (In Mins)"), "fieldname": "time_in_mins", - "fieldtype": "Int", - "width": 140 + "fieldtype": "Float", + "width": 120 }, { "label": _("Sub-assembly BOM Count"), "fieldname": "used_as_subassembly_items", "fieldtype": "Int", - "width": 180 + "width": 200 }] 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 index 97e7e0a7d20..72eed5e0d7c 100644 --- 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 @@ -17,14 +17,12 @@ frappe.query_reports["Cost of Poor Quality Report"] = { 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"), 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 index 77418235b07..88b21170e8b 100644 --- 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 @@ -3,46 +3,65 @@ import frappe from frappe import _ -from frappe.utils import flt def execute(filters=None): - columns, data = [], [] + return get_columns(filters), get_data(filters) - 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"] + if report_filters.get('operation'): + operations = [report_filters.get('operation')] + else: + operations = [d.name for d in operations] - filters = get_filters(report_filters, operations) + job_card = frappe.qb.DocType("Job Card") - job_cards = frappe.get_all("Job Card", fields = fields, - filters = filters) + operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_('operating_cost') + item_code = (job_card.production_item).as_('item_code') - for row in job_cards: - row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) - data.append(row) + query = (frappe.qb + .from_(job_card) + .select(job_card.name, job_card.work_order, item_code, job_card.item_name, + job_card.operation, job_card.serial_no, job_card.batch_no, + job_card.workstation, job_card.total_time_in_mins, job_card.hour_rate, + operating_cost) + .where( + (job_card.docstatus == 1) + & (job_card.is_corrective_job_card == 1)) + .groupby(job_card.name) + ) + query = append_filters(query, report_filters, operations, job_card) + data = query.run(as_dict=True) 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))) +def append_filters(query, report_filters, operations, job_card): + """Append optional filters to query builder. """ - return filters + for field in ("name", "work_order", "operation", "workstation", + "company", "serial_no", "batch_no", "production_item"): + if report_filters.get(field): + if field == 'serial_no': + query = query.where(job_card[field].like('%{}%'.format(report_filters.get(field)))) + elif field == 'operation': + query = query.where(job_card[field].isin(operations)) + else: + query = query.where(job_card[field] == report_filters.get(field)) + + if report_filters.get('from_date') or report_filters.get('to_date'): + job_card_time_log = frappe.qb.DocType("Job Card Time Log") + + query = query.join(job_card_time_log).on(job_card.name == job_card_time_log.parent) + if report_filters.get('from_date'): + query = query.where(job_card_time_log.from_time >= report_filters.get('from_date')) + if report_filters.get('to_date'): + query = query.where(job_card_time_log.to_time <= report_filters.get('to_date')) + + return query def get_columns(filters): return [ diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index 55b1a3f2f9a..aaa231466fd 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -48,7 +48,7 @@ def get_production_plan_item_details(filters, data, order_details): "qty": row.planned_qty, "document_type": "Work Order", "document_name": work_order or "", - "bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"), + "bom_level": 0, "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) }) diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py index 1de472659eb..9f51ded6c77 100644 --- a/erpnext/manufacturing/report/test_reports.py +++ b/erpnext/manufacturing/report/test_reports.py @@ -18,7 +18,7 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ ("BOM Operations Time", {}), ("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}), ("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}), - ("Cost of Poor Quality Report", {}), + ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}), ("Downtime Analysis", {}), ( "Exponential Smoothing Forecasting", diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 65b4d026395..05ca2a84523 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "docstatus": 0, "doctype": "Workspace", @@ -402,7 +402,7 @@ "type": "Link" } ], - "modified": "2021-11-22 17:55:03.524496", + "modified": "2022-01-13 17:40:09.474747", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -411,7 +411,7 @@ "public": 1, "restrict_to_domain": "Manufacturing", "roles": [], - "sequence_id": 17, + "sequence_id": 17.0, "shortcuts": [ { "color": "Green", diff --git a/erpnext/modules.txt b/erpnext/modules.txt index d3b7a17d2e9..c5705c17636 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -14,10 +14,8 @@ Portal Maintenance Education Regional -Restaurant ERPNext Integrations Non Profit -Hotels Quality Management Communication Loan Management diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json index ba2f919d016..fc90475fb32 100644 --- a/erpnext/non_profit/workspace/non_profit/non_profit.json +++ b/erpnext/non_profit/workspace/non_profit/non_profit.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Member\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Non Profit Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Membership\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter Member\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Grant Application\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Membership\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Volunteer\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Donation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tax Exemption Certification (India)\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Member\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Non Profit Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Membership\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chapter\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chapter Member\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Grant Application\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Membership\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Volunteer\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Chapter\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Donation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tax Exemption Certification (India)\",\"col\":4}}]", "creation": "2020-03-02 17:23:47.811421", "docstatus": 0, "doctype": "Workspace", @@ -231,7 +231,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:01.146207", + "modified": "2022-01-13 17:40:50.220877", "modified_by": "Administrator", "module": "Non Profit", "name": "Non Profit", @@ -240,7 +240,7 @@ "public": 1, "restrict_to_domain": "Non Profit", "roles": [], - "sequence_id": 18, + "sequence_id": 18.0, "shortcuts": [ { "label": "Member", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d2d1927c84e..9317afad4d5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -1,4 +1,6 @@ +[pre_model_sync] erpnext.patches.v12_0.update_is_cancelled_field +erpnext.patches.v13_0.add_bin_unique_constraint erpnext.patches.v11_0.rename_production_order_to_work_order erpnext.patches.v11_0.refactor_naming_series erpnext.patches.v11_0.refactor_autoname_naming @@ -202,6 +204,7 @@ execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation") erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 erpnext.patches.v12_0.create_itc_reversal_custom_fields +execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings") erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020 erpnext.patches.v12_0.add_taxjar_integration_field @@ -222,11 +225,11 @@ erpnext.patches.v13_0.set_youtube_video_id erpnext.patches.v13_0.set_app_name erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account +erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02 erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.update_reason_for_resignation_in_employee execute:frappe.delete_doc("Report", "Quoted Item Comparison") erpnext.patches.v13_0.update_member_email_address -erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log erpnext.patches.v13_0.add_po_to_global_search @@ -242,6 +245,8 @@ erpnext.patches.v12_0.add_state_code_for_ladakh erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes erpnext.patches.v13_0.update_vehicle_no_reqd_condition +erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17 +erpnext.patches.v12_0.add_einvoice_summary_report_permissions erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae @@ -252,9 +257,10 @@ erpnext.patches.v12_0.add_gst_category_in_delivery_note erpnext.patches.v12_0.purchase_receipt_status erpnext.patches.v13_0.fix_non_unique_represents_company erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing -erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 +erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 #17-01-2022 erpnext.patches.v13_0.update_shipment_status erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting +erpnext.patches.v12_0.add_ewaybill_validity_field erpnext.patches.v13_0.germany_make_custom_fields erpnext.patches.v13_0.germany_fill_debtor_creditor_number erpnext.patches.v13_0.set_pos_closing_as_failed @@ -266,12 +272,11 @@ erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.update_response_by_variance -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 erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_amt_in_work_order_required_items +erpnext.patches.v12_0.show_einvoice_irn_cancelled_field erpnext.patches.v13_0.delete_orphaned_tables erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16 erpnext.patches.v13_0.update_tds_check_field #3 @@ -279,17 +284,14 @@ erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.update_recipient_email_digest erpnext.patches.v13_0.shopify_deprecation_warning erpnext.patches.v13_0.remove_bad_selling_defaults -erpnext.patches.v13_0.trim_whitespace_from_serial_nos +erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022 erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries -erpnext.patches.v13_0.einvoicing_deprecation_warning execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category") -erpnext.patches.v14_0.delete_einvoicing_doctypes erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021 erpnext.patches.v13_0.set_operation_time_based_on_operating_cost erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 -erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v13_0.fix_invoice_statuses erpnext.patches.v13_0.create_website_items #30-09-2021 erpnext.patches.v13_0.populate_e_commerce_settings @@ -305,9 +307,6 @@ erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries #2 erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry erpnext.patches.v13_0.set_status_in_maintenance_schedule_table erpnext.patches.v13_0.add_default_interview_notification_templates -erpnext.patches.v13_0.create_website_items -erpnext.patches.v13_0.populate_e_commerce_settings -erpnext.patches.v13_0.make_homepage_products_website_items erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting erpnext.patches.v13_0.requeue_failed_reposts erpnext.patches.v13_0.update_job_card_status @@ -318,17 +317,31 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v14_0.delete_healthcare_doctypes erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_pan_field_for_india #2 -erpnext.patches.v14_0.delete_hub_doctypes -erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v13_0.fetch_thumbnail_in_website_items -erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents +erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit +erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022 erpnext.patches.v14_0.migrate_crm_settings erpnext.patches.v13_0.rename_ksa_qr_field +erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 -erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template erpnext.patches.v13_0.update_tax_category_for_rcm execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') erpnext.patches.v14_0.set_payroll_cost_centers erpnext.patches.v13_0.agriculture_deprecation_warning -erpnext.patches.v14_0.delete_agriculture_doctypes +erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.update_exchange_rate_settings +erpnext.patches.v13_0.update_asset_quantity_field +erpnext.patches.v13_0.delete_bank_reconciliation_detail + +[post_model_sync] +erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents +erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template +erpnext.patches.v14_0.delete_shopify_doctypes +erpnext.patches.v14_0.delete_hub_doctypes +erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022 +erpnext.patches.v14_0.delete_agriculture_doctypes +erpnext.patches.v14_0.rearrange_company_fields +erpnext.patches.v14_0.update_leave_notification_template +erpnext.patches.v14_0.restore_einvoice_fields +erpnext.patches.v13_0.update_sane_transfer_against +erpnext.patches.v12_0.add_company_link_to_einvoice_settings diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py new file mode 100644 index 00000000000..e498b673fbf --- /dev/null +++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals + +import frappe + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company or not frappe.db.count('E Invoice User'): + return + + frappe.reload_doc("regional", "doctype", "e_invoice_user") + for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']): + company_name = frappe.db.sql(""" + select dl.link_name from `tabAddress` a, `tabDynamic Link` dl + where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company' + """, (creds.get('gstin'))) + if company_name and len(company_name) > 0: + frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0]) diff --git a/erpnext/patches/v12_0/add_einvoice_status_field.py b/erpnext/patches/v12_0/add_einvoice_status_field.py new file mode 100644 index 00000000000..aeff9ca8413 --- /dev/null +++ b/erpnext/patches/v12_0/add_einvoice_status_field.py @@ -0,0 +1,72 @@ +from __future__ import unicode_literals + +import json + +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + # move hidden einvoice fields to a different section + custom_fields = { + 'Sales Invoice': [ + dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', + print_hide=1, hidden=1), + + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', + no_copy=1, print_hide=1), + + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), + + dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', + no_copy=1, print_hide=1), + + dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', + no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice', + no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code', + no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image', + options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON', + hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1) + ] + } + create_custom_fields(custom_fields, update=True) + + if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'): + frappe.db.sql(''' + UPDATE `tabSales Invoice` SET einvoice_status = 'Pending' + WHERE + posting_date >= '2021-04-01' + AND ifnull(irn, '') = '' + AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '') + AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export') + ''') + + # set appropriate statuses + frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated' + WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''') + + frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled' + WHERE ifnull(irn_cancelled, 0) = 1''') + + # set correct acknowledgement in e-invoices + einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice']) + + if einvoices: + for inv in einvoices: + signed_einvoice = inv.get('signed_einvoice') + if signed_einvoice: + signed_einvoice = json.loads(signed_einvoice) + frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False) + frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False) diff --git a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py new file mode 100644 index 00000000000..e837786138f --- /dev/null +++ b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +import frappe + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + if frappe.db.exists('Report', 'E-Invoice Summary') and \ + not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')): + frappe.get_doc(dict( + doctype='Custom Role', + report='E-Invoice Summary', + roles= [ + dict(role='Accounts User'), + dict(role='Accounts Manager') + ] + )).insert() diff --git a/erpnext/patches/v12_0/add_ewaybill_validity_field.py b/erpnext/patches/v12_0/add_ewaybill_validity_field.py new file mode 100644 index 00000000000..247140d21d0 --- /dev/null +++ b/erpnext/patches/v12_0/add_ewaybill_validity_field.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals + +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + custom_fields = { + 'Sales Invoice': [ + dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1, + depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill') + ] + } + create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py new file mode 100644 index 00000000000..c17666add18 --- /dev/null +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -0,0 +1,59 @@ +from __future__ import unicode_literals + +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + +from erpnext.regional.india.setup import add_permissions, add_print_formats + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + frappe.reload_doc("custom", "doctype", "custom_field") + frappe.reload_doc("regional", "doctype", "e_invoice_settings") + custom_fields = { + 'Sales Invoice': [ + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), + + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), + + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) + ] + } + create_custom_fields(custom_fields, update=True) + add_permissions() + add_print_formats() + + einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + t = { + 'mode_of_transport': [{'default': None}], + 'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}], + 'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], + 'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], + 'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], + 'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], + 'ewaybill': [ + {'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'}, + {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'} + ] + } + + for field, conditions in t.items(): + for c in conditions: + [(prop, value)] = c.items() + frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value) diff --git a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py new file mode 100644 index 00000000000..3f90a03020f --- /dev/null +++ b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py @@ -0,0 +1,14 @@ +from __future__ import unicode_literals + +import frappe + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'}) + if irn_cancelled_field: + frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn') + frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0) diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py index df7875079bc..04010348742 100644 --- a/erpnext/patches/v12_0/update_is_cancelled_field.py +++ b/erpnext/patches/v12_0/update_is_cancelled_field.py @@ -2,14 +2,28 @@ import frappe def execute(): - try: - frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')") - frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')") + #handle type casting for is_cancelled field + module_doctypes = ( + ('stock', 'Stock Ledger Entry'), + ('stock', 'Serial No'), + ('accounts', 'GL Entry') + ) - frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 1 where is_cancelled = 'Yes'") - frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 1 where is_cancelled = 'Yes'") + for module, doctype in module_doctypes: + if (not frappe.db.has_column(doctype, "is_cancelled") + or frappe.db.get_column_type(doctype, "is_cancelled").lower() == "int(1)" + ): + continue - frappe.reload_doc("stock", "doctype", "stock_ledger_entry") - frappe.reload_doc("stock", "doctype", "serial_no") - except Exception: - pass + frappe.db.sql(""" + UPDATE `tab{doctype}` + SET is_cancelled = 0 + where is_cancelled in ('', NULL, 'No')""" + .format(doctype=doctype)) + frappe.db.sql(""" + UPDATE `tab{doctype}` + SET is_cancelled = 1 + where is_cancelled = 'Yes'""" + .format(doctype=doctype)) + + frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py new file mode 100644 index 00000000000..57fbaae9d8d --- /dev/null +++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py @@ -0,0 +1,63 @@ +import frappe + +from erpnext.stock.stock_balance import ( + get_balance_qty_from_sle, + get_indented_qty, + get_ordered_qty, + get_planned_qty, + get_reserved_qty, +) +from erpnext.stock.utils import get_bin + + +def execute(): + delete_broken_bins() + delete_and_patch_duplicate_bins() + +def delete_broken_bins(): + # delete useless bins + frappe.db.sql("delete from `tabBin` where item_code is null or warehouse is null") + +def delete_and_patch_duplicate_bins(): + + duplicate_bins = frappe.db.sql(""" + SELECT + item_code, warehouse, count(*) as bin_count + FROM + tabBin + GROUP BY + item_code, warehouse + HAVING + bin_count > 1 + """, as_dict=1) + + for duplicate_bin in duplicate_bins: + item_code = duplicate_bin.item_code + warehouse = duplicate_bin.warehouse + existing_bins = frappe.get_list("Bin", + filters={ + "item_code": item_code, + "warehouse": warehouse + }, + fields=["name"], + order_by="creation",) + + # keep last one + existing_bins.pop() + + for broken_bin in existing_bins: + frappe.delete_doc("Bin", broken_bin.name) + + qty_dict = { + "reserved_qty": get_reserved_qty(item_code, warehouse), + "indented_qty": get_indented_qty(item_code, warehouse), + "ordered_qty": get_ordered_qty(item_code, warehouse), + "planned_qty": get_planned_qty(item_code, warehouse), + "actual_qty": get_balance_qty_from_sle(item_code, warehouse) + } + + bin = get_bin(item_code, warehouse) + bin.update(qty_dict) + bin.update_reserved_qty_for_production() + bin.update_reserved_qty_for_sub_contracting() + bin.db_update() diff --git a/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py new file mode 100644 index 00000000000..75953b0e304 --- /dev/null +++ b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py @@ -0,0 +1,13 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + + +import frappe + + +def execute(): + + if frappe.db.exists('DocType', 'Bank Reconciliation Detail') and \ + frappe.db.exists('DocType', 'Bank Clearance Detail'): + + frappe.delete_doc("DocType", 'Bank Reconciliation Detail', force=1) diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index c597fe86457..e6eba0a6085 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -12,6 +12,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): + delete_links_from_desktop_icons(report) delete_auto_email_reports(report) check_and_delete_linked_reports(report) @@ -22,3 +23,9 @@ def delete_auto_email_reports(report): auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) + +def delete_links_from_desktop_icons(report): + """ Check for one or multiple Desktop Icons and delete """ + desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"]) + for desktop_icon in desktop_icons: + frappe.delete_doc("Desktop Icon", desktop_icon[0]) \ No newline at end of file diff --git a/erpnext/patches/v13_0/einvoicing_deprecation_warning.py b/erpnext/patches/v13_0/einvoicing_deprecation_warning.py deleted file mode 100644 index e123a55f5ab..00000000000 --- a/erpnext/patches/v13_0/einvoicing_deprecation_warning.py +++ /dev/null @@ -1,9 +0,0 @@ -import click - - -def execute(): - click.secho( - "Indian E-Invoicing integration is moved to a separate app and will be removed from ERPNext in version-14.\n" - "Please install the app to continue using the integration: https://github.com/frappe/erpnext_gst_compliance", - fg="yellow", - ) diff --git a/erpnext/patches/v13_0/hospitality_deprecation_warning.py b/erpnext/patches/v13_0/hospitality_deprecation_warning.py new file mode 100644 index 00000000000..2708b2ccd3b --- /dev/null +++ b/erpnext/patches/v13_0/hospitality_deprecation_warning.py @@ -0,0 +1,10 @@ +import click + + +def execute(): + + click.secho( + "Hospitality domain is moved to a separate app and will be removed from ERPNext in version-14.\n" + "When upgrading to ERPNext version-14, please install the app to continue using the Hospitality domain: https://github.com/frappe/hospitality", + fg="yellow", + ) diff --git a/erpnext/patches/v13_0/make_non_standard_user_type.py b/erpnext/patches/v13_0/make_non_standard_user_type.py index a7bdf93332a..ff241a3fd93 100644 --- a/erpnext/patches/v13_0/make_non_standard_user_type.py +++ b/erpnext/patches/v13_0/make_non_standard_user_type.py @@ -10,8 +10,15 @@ from erpnext.setup.install import add_non_standard_user_types def execute(): doctype_dict = { 'projects': ['Timesheet'], - 'payroll': ['Salary Slip', 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission'], - 'hr': ['Employee', 'Expense Claim', 'Leave Application', 'Attendance Request', 'Compensatory Leave Request'] + 'payroll': [ + 'Salary Slip', 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission', + 'Employee Benefit Application', 'Employee Benefit Claim' + ], + 'hr': [ + 'Employee', 'Expense Claim', 'Leave Application', 'Attendance Request', 'Compensatory Leave Request', + 'Holiday List', 'Employee Advance', 'Training Program', 'Training Feedback', + 'Shift Request', 'Employee Grievance', 'Employee Referral', 'Travel Request' + ] } for module, doctypes in doctype_dict.items(): diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py index 7a2a2539670..2d35ea34587 100644 --- a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py +++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py @@ -5,6 +5,9 @@ from erpnext.regional.india.setup import make_custom_fields def execute(): if frappe.get_all('Company', filters = {'country': 'India'}): + frappe.reload_doc('accounts', 'doctype', 'POS Invoice') + frappe.reload_doc('accounts', 'doctype', 'POS Invoice Item') + make_custom_fields() if not frappe.db.exists('Party Type', 'Donor'): diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py index 8a9633d8964..4ec22e9d0e1 100644 --- a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py +++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py @@ -9,13 +9,15 @@ def execute(): from `tabStock Ledger Entry` where is_cancelled = 0 - and (serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s) + and ( serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s + or serial_no = %s ) """, ( " %", # leading whitespace "% ", # trailing whitespace "%\n %", # leading whitespace on newline "% \n%", # trailing whitespace on newline + "\n", # just new line ), as_dict=True, ) diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py index 55fd465b204..60466eb86a4 100644 --- a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py +++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py @@ -37,4 +37,4 @@ def execute(): jc.production_item = wo.production_item, jc.item_name = wo.item_name WHERE jc.work_order = wo.name and IFNULL(jc.production_item, "") = "" - """) + """) \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_asset_quantity_field.py b/erpnext/patches/v13_0/update_asset_quantity_field.py new file mode 100644 index 00000000000..47884d1968f --- /dev/null +++ b/erpnext/patches/v13_0/update_asset_quantity_field.py @@ -0,0 +1,8 @@ +import frappe + + +def execute(): + if frappe.db.count('Asset'): + frappe.reload_doc("assets", "doctype", "Asset") + asset = frappe.qb.DocType('Asset') + frappe.qb.update(asset).set(asset.asset_quantity, 1).run() \ 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 deleted file mode 100644 index 499412ee270..00000000000 --- a/erpnext/patches/v13_0/update_level_in_bom.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2020, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -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/patches/v13_0/update_maintenance_schedule_field_in_visit.py b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py new file mode 100644 index 00000000000..450c00e4212 --- /dev/null +++ b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py @@ -0,0 +1,22 @@ + +import frappe + + +def execute(): + # Updates the Maintenance Schedule link to fetch serial nos + from frappe.query_builder.functions import Coalesce + mvp = frappe.qb.DocType('Maintenance Visit Purpose') + mv = frappe.qb.DocType('Maintenance Visit') + + frappe.qb.update( + mv + ).join( + mvp + ).on(mvp.parent == mv.name).set( + mv.maintenance_schedule, + Coalesce(mvp.prevdoc_docname, '') + ).where( + (mv.maintenance_type == "Scheduled") + & (mvp.prevdoc_docname.notnull()) + & (mv.docstatus < 2) + ).run(as_dict=1) diff --git a/erpnext/patches/v13_0/update_sane_transfer_against.py b/erpnext/patches/v13_0/update_sane_transfer_against.py new file mode 100644 index 00000000000..a163d385843 --- /dev/null +++ b/erpnext/patches/v13_0/update_sane_transfer_against.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + bom = frappe.qb.DocType("BOM") + + (frappe.qb + .update(bom) + .set(bom.transfer_material_against, "Work Order") + .where(bom.with_operations == 0) + ).run() diff --git a/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py new file mode 100644 index 00000000000..e43a8bad8ea --- /dev/null +++ b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py @@ -0,0 +1,18 @@ +import frappe + + +def execute(): + + doctype = "Stock Reconciliation Item" + + if not frappe.db.has_column(doctype, "current_serial_no"): + # nothing to fix if column doesn't exist + return + + sr_item = frappe.qb.DocType(doctype) + + (frappe.qb + .update(sr_item) + .set(sr_item.current_serial_no, None) + .where(sr_item.current_qty == 0) + ).run() diff --git a/erpnext/patches/v14_0/__init__.py b/erpnext/patches/v14_0/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py index 120182a80e3..2a8b6ef746a 100644 --- a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py +++ b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py @@ -5,9 +5,6 @@ from frappe import _ def execute(): - frappe.reload_doc("email", "doctype", "email_template") - frappe.reload_doc("hr", "doctype", "hr_settings") - template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification")) if not template: base_path = frappe.get_app_path("erpnext", "hr", "doctype") diff --git a/erpnext/patches/v14_0/delete_einvoicing_doctypes.py b/erpnext/patches/v14_0/delete_einvoicing_doctypes.py deleted file mode 100644 index a3a8149be32..00000000000 --- a/erpnext/patches/v14_0/delete_einvoicing_doctypes.py +++ /dev/null @@ -1,10 +0,0 @@ -import frappe - - -def execute(): - frappe.delete_doc('DocType', 'E Invoice Settings', ignore_missing=True) - frappe.delete_doc('DocType', 'E Invoice User', ignore_missing=True) - frappe.delete_doc('Report', 'E-Invoice Summary', ignore_missing=True) - frappe.delete_doc('Print Format', 'GST E-Invoice', ignore_missing=True) - frappe.delete_doc('Custom Field', 'Sales Invoice-eway_bill_cancelled', ignore_missing=True) - frappe.delete_doc('Custom Field', 'Sales Invoice-irn_cancelled', ignore_missing=True) diff --git a/erpnext/patches/v14_0/delete_healthcare_doctypes.py b/erpnext/patches/v14_0/delete_healthcare_doctypes.py index 28fc01beab5..3a4f8f537d1 100644 --- a/erpnext/patches/v14_0/delete_healthcare_doctypes.py +++ b/erpnext/patches/v14_0/delete_healthcare_doctypes.py @@ -47,3 +47,18 @@ def execute(): frappe.delete_doc("DocType", doctype, ignore_missing=True) frappe.delete_doc("Module Def", "Healthcare", ignore_missing=True, force=True) + + custom_fields = { + 'Sales Invoice': ['patient', 'patient_name', 'ref_practitioner'], + 'Sales Invoice Item': ['reference_dt', 'reference_dn'], + 'Stock Entry': ['inpatient_medication_entry'], + 'Stock Entry Detail': ['patient', 'inpatient_medication_entry_child'], + } + for doc, fields in custom_fields.items(): + filters = { + 'dt': doc, + 'fieldname': ['in', fields] + } + records = frappe.get_all('Custom Field', filters=filters, pluck='name') + for record in records: + frappe.delete_doc('Custom Field', record, ignore_missing=True, force=True) diff --git a/erpnext/patches/v14_0/delete_hospitality_doctypes.py b/erpnext/patches/v14_0/delete_hospitality_doctypes.py new file mode 100644 index 00000000000..d0216f80d3d --- /dev/null +++ b/erpnext/patches/v14_0/delete_hospitality_doctypes.py @@ -0,0 +1,32 @@ +import frappe + + +def execute(): + modules = ['Hotels', 'Restaurant'] + + for module in modules: + frappe.delete_doc("Module Def", module, ignore_missing=True, force=True) + + frappe.delete_doc("Workspace", module, ignore_missing=True, force=True) + + reports = frappe.get_all("Report", {"module": module, "is_standard": "Yes"}, pluck='name') + for report in reports: + frappe.delete_doc("Report", report, ignore_missing=True, force=True) + + dashboards = frappe.get_all("Dashboard", {"module": module, "is_standard": 1}, pluck='name') + for dashboard in dashboards: + frappe.delete_doc("Dashboard", dashboard, ignore_missing=True, force=True) + + doctypes = frappe.get_all("DocType", {"module": module, "custom": 0}, pluck='name') + for doctype in doctypes: + frappe.delete_doc("DocType", doctype, ignore_missing=True) + + custom_fields = [ + {"dt": "Sales Invoice", "fieldname": "restaurant"}, + {"dt": "Sales Invoice", "fieldname": "restaurant_table"}, + {"dt": "Price List", "fieldname": "restaurant_menu"}, + ] + + for field in custom_fields: + custom_field = frappe.db.get_value("Custom Field", field) + frappe.delete_doc("Custom Field", custom_field, ignore_missing=True) diff --git a/erpnext/patches/v14_0/rearrange_company_fields.py b/erpnext/patches/v14_0/rearrange_company_fields.py new file mode 100644 index 00000000000..fd7eb7f750a --- /dev/null +++ b/erpnext/patches/v14_0/rearrange_company_fields.py @@ -0,0 +1,28 @@ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def execute(): + custom_fields = { + 'Company': [ + dict(fieldname='hra_section', label='HRA Settings', + fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1), + dict(fieldname='basic_component', label='Basic Component', + fieldtype='Link', options='Salary Component', insert_after='hra_section'), + dict(fieldname='hra_component', label='HRA Component', + fieldtype='Link', options='Salary Component', insert_after='basic_component'), + dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'), + dict(fieldname='arrear_component', label='Arrear Component', + fieldtype='Link', options='Salary Component', insert_after='hra_column_break'), + dict(fieldname='non_profit_section', label='Non Profit Settings', + fieldtype='Section Break', insert_after='arrear_component', collapsible=1), + dict(fieldname='company_80g_number', label='80G Number', + fieldtype='Data', insert_after='non_profit_section'), + dict(fieldname='with_effect_from', label='80G With Effect From', + fieldtype='Date', insert_after='company_80g_number'), + dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'), + dict(fieldname='pan_details', label='PAN Number', + fieldtype='Data', insert_after='non_profit_column_break') + ] + } + + create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v14_0/restore_einvoice_fields.py b/erpnext/patches/v14_0/restore_einvoice_fields.py new file mode 100644 index 00000000000..c4431fb9dbe --- /dev/null +++ b/erpnext/patches/v14_0/restore_einvoice_fields.py @@ -0,0 +1,24 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + +from erpnext.regional.india.setup import add_permissions, add_print_formats + + +def execute(): + # restores back the 2 custom fields that was deleted while removing e-invoicing from v14 + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + custom_fields = { + 'Sales Invoice': [ + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + ] + } + create_custom_fields(custom_fields, update=True) + add_permissions() + add_print_formats() diff --git a/erpnext/patches/v14_0/update_leave_notification_template.py b/erpnext/patches/v14_0/update_leave_notification_template.py new file mode 100644 index 00000000000..e744054a2f2 --- /dev/null +++ b/erpnext/patches/v14_0/update_leave_notification_template.py @@ -0,0 +1,17 @@ +import os + +import frappe +from frappe import _ + + +def execute(): + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html")) + + template = frappe.db.exists("Email Template", _("Leave Approval Notification")) + if template: + frappe.db.set_value("Email Template", template, "response", response) + + template = frappe.db.exists("Email Template", _("Leave Status Notification")) + if template: + frappe.db.set_value("Email Template", template, "response", response) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json index d9efe458dcf..9c897a7c697 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.json +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.json @@ -204,10 +204,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-05-26 11:10:00.812698", + "modified": "2022-01-19 12:56:51.765353", "modified_by": "Administrator", "module": "Payroll", "name": "Additional Salary", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -239,8 +240,10 @@ "write": 1 } ], + "search_fields": "employee_name", "sort_field": "modified", "sort_order": "DESC", - "title_field": "employee", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json index 83326975b0a..2e4b64e9daf 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json @@ -147,10 +147,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:35:08.940087", + "modified": "2022-01-19 12:58:31.664468", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -212,8 +213,10 @@ } ], "quick_entry": 1, + "search_fields": "employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json index b3bac01818f..5deb0a5eca6 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json @@ -144,10 +144,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:37:21.024625", + "modified": "2022-01-19 12:59:15.699118", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Claim", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -208,8 +209,10 @@ "write": 1 } ], + "search_fields": "employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json index 0d10b2c19ae..64fb8c5c98e 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json @@ -94,10 +94,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:38:20.332316", + "modified": "2022-01-19 12:52:19.850710", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Incentive", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -136,8 +137,10 @@ "write": 1 } ], + "search_fields": "employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_other_income/employee_other_income.json b/erpnext/payroll/doctype/employee_other_income/employee_other_income.json index 14f63e4fdd9..04ce9f79a17 100644 --- a/erpnext/payroll/doctype/employee_other_income/employee_other_income.json +++ b/erpnext/payroll/doctype/employee_other_income/employee_other_income.json @@ -76,10 +76,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:55:17.604688", + "modified": "2022-01-19 12:58:43.255900", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Other Income", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -129,7 +130,10 @@ } ], "quick_entry": 1, + "search_fields": "employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json index b247d266ae4..5ef373e8876 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json @@ -119,10 +119,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:39:59.237361", + "modified": "2022-01-19 12:58:54.707871", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Declaration", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -186,7 +187,10 @@ "write": 1 } ], + "search_fields": "employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json index 77b107ef4a3..bb90051e5d1 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json @@ -142,10 +142,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:41:13.723339", + "modified": "2022-01-19 12:58:24.244546", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Proof Submission", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -209,7 +210,10 @@ "write": 1 } ], + "search_fields": "employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json index 48a9ce4759a..197089567df 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.json +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -167,10 +167,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-07-02 15:05:57.396398", + "modified": "2022-01-19 12:54:37.306145", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -198,6 +199,9 @@ "write": 1 } ], + "search_fields": "employee_name", "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [], + "title_field": "employee_name" } \ No newline at end of file diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index ed3fa5befc2..db88c0643c2 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -61,6 +61,8 @@ class PayrollEntry(Document): def on_cancel(self): frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` where payroll_entry=%s """, (self.name))) + self.db_set("salary_slips_created", 0) + self.db_set("salary_slips_submitted", 0) def get_emp_list(self): """ diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json index 7ea6210c7ad..f8d8bb46deb 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json @@ -105,10 +105,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:43:28.363644", + "modified": "2022-01-19 12:57:37.898953", "modified_by": "Administrator", "module": "Payroll", "name": "Retention Bonus", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -163,7 +164,10 @@ "share": 1 } ], + "search_fields": "employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 4e40e13be01..fe8e22cedf2 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -637,7 +637,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2021-12-23 11:47:47.098248", + "modified": "2022-01-19 12:45:54.999345", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", @@ -673,9 +673,11 @@ "role": "Employee" } ], + "search_fields": "employee_name", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "employee", "title_field": "employee_name" -} +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index c0e005ad992..bcf981b74dc 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -994,6 +994,8 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non )) leave_application.submit() + return leave_application + def setup_test(): make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, company_list=["_Test Company"]) diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json index 197ab5f25b7..613246e732f 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -162,7 +162,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-12-23 17:28:09.794444", + "modified": "2022-01-19 12:43:54.439073", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure Assignment", @@ -209,6 +209,7 @@ "write": 1 } ], + "search_fields": "employee_name, salary_structure", "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/erpnext/payroll/workspace/payroll/payroll.json b/erpnext/payroll/workspace/payroll/payroll.json index 7246dae5bc9..762bea02c73 100644 --- a/erpnext/payroll/workspace/payroll/payroll.json +++ b/erpnext/payroll/workspace/payroll/payroll.json @@ -5,7 +5,7 @@ "label": "Outgoing Salary" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Payroll\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Salary Structure\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Payroll Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Salary Slip\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Income Tax Slab\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Salary Register\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payroll\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Taxation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Compensations\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Payroll\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Salary Structure\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payroll Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Salary Slip\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Income Tax Slab\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Salary Register\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Payroll\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Taxation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Compensations\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "creation": "2020-05-27 19:54:23.405607", "docstatus": 0, "doctype": "Workspace", @@ -312,7 +312,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:01.335325", + "modified": "2022-01-13 17:41:19.098813", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll", @@ -321,7 +321,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 19, + "sequence_id": 19.0, "shortcuts": [ { "label": "Salary Structure", diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 2570df70261..1cda0a08c47 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -235,13 +235,13 @@ { "fieldname": "actual_start_date", "fieldtype": "Data", - "label": "Actual Start Date", + "label": "Actual Start Date (via Time Sheet)", "read_only": 1 }, { "fieldname": "actual_time", "fieldtype": "Float", - "label": "Actual Time (in Hours)", + "label": "Actual Time (in Hours via Time Sheet)", "read_only": 1 }, { @@ -251,7 +251,7 @@ { "fieldname": "actual_end_date", "fieldtype": "Date", - "label": "Actual End Date", + "label": "Actual End Date (via Time Sheet)", "oldfieldname": "act_completion_date", "oldfieldtype": "Date", "read_only": 1 @@ -458,10 +458,11 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2021-04-28 16:36:11.654632", + "modified": "2022-01-29 13:58:27.712714", "modified_by": "Administrator", "module": "Projects", "name": "Project", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -499,6 +500,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "project_name", "track_seen": 1 diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index ef4740d9eef..81822085a58 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -249,7 +249,7 @@ { "fieldname": "actual_time", "fieldtype": "Float", - "label": "Actual Time (in hours)", + "label": "Actual Time (in Hours via Time Sheet)", "read_only": 1 }, { @@ -397,10 +397,11 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2021-04-16 12:46:51.556741", + "modified": "2022-01-29 13:58:47.005241", "modified_by": "Administrator", "module": "Projects", "name": "Task", + "naming_rule": "Expression (old style)", "nsm_parent_field": "parent_task", "owner": "Administrator", "permissions": [ @@ -421,6 +422,7 @@ "show_preview_popup": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "project", "title_field": "subject", "track_seen": 1 diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 148d8ba29c2..989bcd1670d 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -5,7 +5,7 @@ import datetime import unittest import frappe -from frappe.utils import add_months, now_datetime, nowdate +from frappe.utils import add_months, add_to_date, now_datetime, nowdate from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.hr.doctype.employee.test_employee import make_employee @@ -151,6 +151,27 @@ class TestTimesheet(unittest.TestCase): settings.ignore_employee_time_overlap = initial_setting settings.save() + def test_to_time(self): + emp = make_employee("test_employee_6@salary.com") + from_time = now_datetime() + + timesheet = frappe.new_doc("Timesheet") + timesheet.employee = emp + timesheet.append( + 'time_logs', + { + "billable": 1, + "activity_type": "_Test Activity Type", + "from_time": from_time, + "hours": 2, + "company": "_Test Company" + } + ) + timesheet.save() + + to_time = timesheet.time_logs[0].to_time + self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True)) + def make_salary_structure_for_timesheet(employee, company=None): salary_structure_name = "Timesheet Salary Structure Test" diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index e92785e06cf..dd0b5f90f4d 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt, getdate, time_diff_in_hours +from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours from erpnext.controllers.queries import get_match_cond from erpnext.hr.utils import validate_active_employee @@ -136,10 +136,19 @@ class Timesheet(Document): def validate_time_logs(self): for data in self.get('time_logs'): + self.set_to_time(data) self.validate_overlap(data) self.set_project(data) self.validate_project(data) + def set_to_time(self, data): + if not (data.from_time and data.hours): + return + + _to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True) + if data.to_time != _to_time: + data.to_time = _to_time + def validate_overlap(self, data): settings = frappe.get_single('Projects Settings') self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap) diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index 1df2b089839..c5a047d5cb9 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -5,7 +5,7 @@ "label": "Open Projects" } ], - "content": "[{\"type\": \"chart\", \"data\": {\"chart_name\": \"Open Projects\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Task\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Project\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Timesheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Project Billing Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Projects\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Time Tracking\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"chart\",\"data\":{\"chart_name\":\"Open Projects\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Task\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Timesheet\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project Billing Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Projects\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Time Tracking\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "creation": "2020-03-02 15:46:04.874669", "docstatus": 0, "doctype": "Workspace", @@ -194,7 +194,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:01.540147", + "modified": "2022-01-13 17:41:55.163878", "modified_by": "Administrator", "module": "Projects", "name": "Projects", @@ -203,7 +203,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 20, + "sequence_id": 20.0, "shortcuts": [ { "color": "Blue", diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 597d77c6e9f..08270bdea1a 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -592,6 +592,6 @@ function check_can_calculate_pending_qty(me) { && doc.fg_completed_qty && erpnext.stock.bom && erpnext.stock.bom.name === doc.bom_no; - const itemChecks = !!item; + const itemChecks = !!item && !item.allow_alternative_item; return docChecks && itemChecks; } diff --git a/erpnext/quality_management/workspace/quality/quality.json b/erpnext/quality_management/workspace/quality/quality.json index ae284701824..3effd59d8ee 100644 --- a/erpnext/quality_management/workspace/quality/quality.json +++ b/erpnext/quality_management/workspace/quality/quality.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Goal\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Procedure\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Inspection\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Review\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Action\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Non Conformance\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Goal and Procedure\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Feedback\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Meeting\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Review and Action\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Goal\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Procedure\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Inspection\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Review\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Action\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Non Conformance\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Goal and Procedure\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Feedback\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Meeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Review and Action\",\"col\":4}}]", "creation": "2020-03-02 15:49:28.632014", "docstatus": 0, "doctype": "Workspace", @@ -142,7 +142,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:01.699913", + "modified": "2022-01-13 17:42:20.105187", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality", @@ -151,7 +151,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 21, + "sequence_id": 21.0, "shortcuts": [ { "color": "Grey", diff --git a/erpnext/demo/setup/__init__.py b/erpnext/regional/doctype/e_invoice_request_log/__init__.py similarity index 100% rename from erpnext/demo/setup/__init__.py rename to erpnext/regional/doctype/e_invoice_request_log/__init__.py diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js new file mode 100644 index 00000000000..7b7ba964e5e --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.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('E Invoice Request Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json new file mode 100644 index 00000000000..3034370feac --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json @@ -0,0 +1,102 @@ +{ + "actions": [], + "autoname": "EINV-REQ-.#####", + "creation": "2020-12-08 12:54:08.175992", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "url", + "headers", + "response", + "column_break_7", + "timestamp", + "reference_invoice", + "data" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + }, + { + "fieldname": "reference_invoice", + "fieldtype": "Data", + "label": "Reference Invoice" + }, + { + "fieldname": "headers", + "fieldtype": "Code", + "label": "Headers", + "options": "JSON" + }, + { + "fieldname": "data", + "fieldtype": "Code", + "label": "Data", + "options": "JSON" + }, + { + "default": "Now", + "fieldname": "timestamp", + "fieldtype": "Datetime", + "label": "Timestamp" + }, + { + "fieldname": "response", + "fieldtype": "Code", + "label": "Response", + "options": "JSON" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-01-13 12:06:57.253111", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice Request Log", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py new file mode 100644 index 00000000000..c89552d7824 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class EInvoiceRequestLog(Document): + pass diff --git a/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py new file mode 100644 index 00000000000..091cc88e454 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py @@ -0,0 +1,11 @@ +# -*- 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 TestEInvoiceRequestLog(unittest.TestCase): + pass diff --git a/erpnext/demo/user/__init__.py b/erpnext/regional/doctype/e_invoice_settings/__init__.py similarity index 100% rename from erpnext/demo/user/__init__.py rename to erpnext/regional/doctype/e_invoice_settings/__init__.py diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js new file mode 100644 index 00000000000..54e488610df --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -0,0 +1,11 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('E Invoice Settings', { + refresh(frm) { + const docs_link = 'https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing'; + frm.dashboard.set_headline( + __("Read {0} for more information on E Invoicing features.", [`documentation`]) + ); + } +}); diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json new file mode 100644 index 00000000000..16b29633010 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -0,0 +1,97 @@ +{ + "actions": [], + "creation": "2020-09-24 16:23:16.235722", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable", + "section_break_2", + "sandbox_mode", + "applicable_from", + "credentials", + "advanced_settings_section", + "client_id", + "column_break_8", + "client_secret", + "auth_token", + "token_expiry" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "depends_on": "enable", + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "auth_token", + "fieldtype": "Data", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "token_expiry", + "fieldtype": "Datetime", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "credentials", + "fieldtype": "Table", + "label": "Credentials", + "mandatory_depends_on": "enable", + "options": "E Invoice User" + }, + { + "default": "0", + "fieldname": "sandbox_mode", + "fieldtype": "Check", + "label": "Sandbox Mode" + }, + { + "fieldname": "applicable_from", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Applicable From", + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "advanced_settings_section", + "fieldtype": "Section Break", + "label": "Advanced Settings" + }, + { + "fieldname": "client_id", + "fieldtype": "Data", + "label": "Client ID" + }, + { + "fieldname": "client_secret", + "fieldtype": "Password", + "label": "Client Secret" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2021-11-16 19:50:28.029517", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice Settings", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py new file mode 100644 index 00000000000..342d583cc0d --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class EInvoiceSettings(Document): + def validate(self): + if self.enable and not self.credentials: + frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.')) diff --git a/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py new file mode 100644 index 00000000000..10770deb0ee --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py @@ -0,0 +1,11 @@ +# -*- 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 TestEInvoiceSettings(unittest.TestCase): + pass diff --git a/erpnext/hotels/__init__.py b/erpnext/regional/doctype/e_invoice_user/__init__.py similarity index 100% rename from erpnext/hotels/__init__.py rename to erpnext/regional/doctype/e_invoice_user/__init__.py diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json new file mode 100644 index 00000000000..a65b1ca7ca8 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json @@ -0,0 +1,57 @@ +{ + "actions": [], + "creation": "2020-12-22 15:02:46.229474", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "gstin", + "username", + "password" + ], + "fields": [ + { + "fieldname": "gstin", + "fieldtype": "Data", + "in_list_view": 1, + "label": "GSTIN", + "reqd": 1 + }, + { + "fieldname": "username", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Username", + "reqd": 1 + }, + { + "fieldname": "password", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Password", + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-03-22 12:16:56.365616", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice User", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py new file mode 100644 index 00000000000..4e0e89c09a5 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class EInvoiceUser(Document): + pass diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index 2d1e02eadba..ec271a10294 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -1,11 +1,11 @@ import datetime import zipfile from csv import QUOTE_NONNUMERIC +from io import BytesIO import frappe import pandas as pd from frappe import _ -from six import BytesIO from .datev_constants import DataCategory diff --git a/erpnext/hotels/doctype/__init__.py b/erpnext/regional/india/e_invoice/__init__.py similarity index 100% rename from erpnext/hotels/doctype/__init__.py rename to erpnext/regional/india/e_invoice/__init__.py diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json new file mode 100644 index 00000000000..78e56518dff --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_item_template.json @@ -0,0 +1,31 @@ +{{ + "SlNo": "{item.sr_no}", + "PrdDesc": "{item.description}", + "IsServc": "{item.is_service_item}", + "HsnCd": "{item.gst_hsn_code}", + "Barcde": "{item.barcode}", + "Unit": "{item.uom}", + "Qty": "{item.qty}", + "FreeQty": "{item.free_qty}", + "UnitPrice": "{item.unit_rate}", + "TotAmt": "{item.gross_amount}", + "Discount": "{item.discount_amount}", + "AssAmt": "{item.taxable_value}", + "PrdSlNo": "{item.serial_no}", + "GstRt": "{item.tax_rate}", + "IgstAmt": "{item.igst_amount}", + "CgstAmt": "{item.cgst_amount}", + "SgstAmt": "{item.sgst_amount}", + "CesRt": "{item.cess_rate}", + "CesAmt": "{item.cess_amount}", + "CesNonAdvlAmt": "{item.cess_nadv_amount}", + "StateCesRt": "{item.state_cess_rate}", + "StateCesAmt": "{item.state_cess_amount}", + "StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}", + "OthChrg": "{item.other_charges}", + "TotItemVal": "{item.total_value}", + "BchDtls": {{ + "Nm": "{item.batch_no}", + "ExpDt": "{item.batch_expiry_date}" + }} +}} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json new file mode 100644 index 00000000000..c2a28f20494 --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -0,0 +1,110 @@ +{{ + "Version": "1.1", + "TranDtls": {{ + "TaxSch": "{transaction_details.tax_scheme}", + "SupTyp": "{transaction_details.supply_type}", + "RegRev": "{transaction_details.reverse_charge}", + "EcmGstin": "{transaction_details.ecom_gstin}", + "IgstOnIntra": "{transaction_details.igst_on_intra}" + }}, + "DocDtls": {{ + "Typ": "{doc_details.invoice_type}", + "No": "{doc_details.invoice_name}", + "Dt": "{doc_details.invoice_date}" + }}, + "SellerDtls": {{ + "Gstin": "{seller_details.gstin}", + "LglNm": "{seller_details.legal_name}", + "TrdNm": "{seller_details.trade_name}", + "Loc": "{seller_details.location}", + "Pin": "{seller_details.pincode}", + "Stcd": "{seller_details.state_code}", + "Addr1": "{seller_details.address_line1}", + "Addr2": "{seller_details.address_line2}", + "Ph": "{seller_details.phone}", + "Em": "{seller_details.email}" + }}, + "BuyerDtls": {{ + "Gstin": "{buyer_details.gstin}", + "LglNm": "{buyer_details.legal_name}", + "TrdNm": "{buyer_details.trade_name}", + "Addr1": "{buyer_details.address_line1}", + "Addr2": "{buyer_details.address_line2}", + "Loc": "{buyer_details.location}", + "Pin": "{buyer_details.pincode}", + "Stcd": "{buyer_details.state_code}", + "Ph": "{buyer_details.phone}", + "Em": "{buyer_details.email}", + "Pos": "{buyer_details.place_of_supply}" + }}, + "DispDtls": {{ + "Nm": "{dispatch_details.legal_name}", + "Addr1": "{dispatch_details.address_line1}", + "Addr2": "{dispatch_details.address_line2}", + "Loc": "{dispatch_details.location}", + "Pin": "{dispatch_details.pincode}", + "Stcd": "{dispatch_details.state_code}" + }}, + "ShipDtls": {{ + "Gstin": "{shipping_details.gstin}", + "LglNm": "{shipping_details.legal_name}", + "TrdNm": "{shipping_details.trader_name}", + "Addr1": "{shipping_details.address_line1}", + "Addr2": "{shipping_details.address_line2}", + "Loc": "{shipping_details.location}", + "Pin": "{shipping_details.pincode}", + "Stcd": "{shipping_details.state_code}" + }}, + "ItemList": [ + {item_list} + ], + "ValDtls": {{ + "AssVal": "{invoice_value_details.base_total}", + "CgstVal": "{invoice_value_details.total_cgst_amt}", + "SgstVal": "{invoice_value_details.total_sgst_amt}", + "IgstVal": "{invoice_value_details.total_igst_amt}", + "CesVal": "{invoice_value_details.total_cess_amt}", + "Discount": "{invoice_value_details.invoice_discount_amt}", + "RndOffAmt": "{invoice_value_details.round_off}", + "OthChrg": "{invoice_value_details.total_other_charges}", + "TotInvVal": "{invoice_value_details.base_grand_total}", + "TotInvValFc": "{invoice_value_details.grand_total}" + }}, + "PayDtls": {{ + "Nm": "{payment_details.payee_name}", + "AccDet": "{payment_details.account_no}", + "Mode": "{payment_details.mode_of_payment}", + "FinInsBr": "{payment_details.ifsc_code}", + "PayTerm": "{payment_details.terms}", + "PaidAmt": "{payment_details.paid_amount}", + "PaymtDue": "{payment_details.outstanding_amount}" + }}, + "RefDtls": {{ + "DocPerdDtls": {{ + "InvStDt": "{period_details.start_date}", + "InvEndDt": "{period_details.end_date}" + }}, + "PrecDocDtls": [{{ + "InvNo": "{prev_doc_details.invoice_name}", + "InvDt": "{prev_doc_details.invoice_date}" + }}] + }}, + "ExpDtls": {{ + "ShipBNo": "{export_details.bill_no}", + "ShipBDt": "{export_details.bill_date}", + "Port": "{export_details.port}", + "ForCur": "{export_details.foreign_curr_code}", + "CntCode": "{export_details.country_code}", + "ExpDuty": "{export_details.export_duty}" + }}, + "EwbDtls": {{ + "TransId": "{eway_bill_details.gstin}", + "TransName": "{eway_bill_details.name}", + "TransMode": "{eway_bill_details.mode_of_transport}", + "Distance": "{eway_bill_details.distance}", + "TransDocNo": "{eway_bill_details.document_name}", + "TransDocDt": "{eway_bill_details.document_date}", + "VehNo": "{eway_bill_details.vehicle_no}", + "VehType": "{eway_bill_details.vehicle_type}" + }} +}} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json new file mode 100644 index 00000000000..f4a3542a60e --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_validation.json @@ -0,0 +1,957 @@ +{ + "Version": { + "type": "string", + "minLength": 1, + "maxLength": 6, + "description": "Version of the schema" + }, + "Irn": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "Invoice Reference Number" + }, + "TranDtls": { + "type": "object", + "properties": { + "TaxSch": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["GST"], + "description": "GST- Goods and Services Tax Scheme" + }, + "SupTyp": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"], + "description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export" + }, + "RegRev": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"], + "description": "Y- whether the tax liability is payable under reverse charge" + }, + "EcmGstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "description": "E-Commerce GSTIN", + "validationMsg": "E-Commerce GSTIN is invalid" + }, + "IgstOnIntra": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"], + "description": "Y- indicates the supply is intra state but chargeable to IGST" + } + }, + "required": ["TaxSch", "SupTyp"] + }, + "DocDtls": { + "type": "object", + "properties": { + "Typ": { + "type": "string", + "minLength": 3, + "maxLength": 3, + "enum": ["INV", "CRN", "DBN"], + "description": "Document Type" + }, + "No": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$", + "description": "Document Number", + "validationMsg": "Document Number should not be starting with 0, / and -" + }, + "Dt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Document Date" + } + }, + "required": ["Typ", "No", "Dt"] + }, + "SellerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "description": "Supplier GSTIN", + "validationMsg": "Company GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Legal Name" + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Tradename" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 50, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "Supplier State Code" + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12, + "description": "Phone" + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100, + "description": "Email-Id" + } + }, + "required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "BuyerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 3, + "maxLength": 15, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "description": "Buyer GSTIN", + "validationMsg": "Customer GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Legal Name" + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Trade Name" + }, + "Pos": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "Place of Supply State code" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "Buyer State Code" + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12, + "description": "Phone" + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100, + "description": "Email-Id" + } + }, + "required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"] + }, + "DispDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Dispatch Address Name" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "State Code" + } + }, + "required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ShipDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "maxLength": 15, + "minLength": 3, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "description": "Shipping Address GSTIN", + "validationMsg": "Shipping Address GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Legal Name" + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Trade Name" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "State Code" + } + }, + "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ItemList": { + "type": "Array", + "properties": { + "SlNo": { + "type": "string", + "minLength": 1, + "maxLength": 6, + "description": "Serial No. of Item" + }, + "PrdDesc": { + "type": "string", + "minLength": 3, + "maxLength": 300, + "description": "Item Name" + }, + "IsServc": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"], + "description": "Is Service Item" + }, + "HsnCd": { + "type": "string", + "minLength": 4, + "maxLength": 8, + "description": "HSN Code" + }, + "Barcde": { + "type": "string", + "minLength": 3, + "maxLength": 30, + "description": "Barcode" + }, + "Qty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999, + "description": "Quantity" + }, + "FreeQty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999, + "description": "Free Quantity" + }, + "Unit": { + "type": "string", + "minLength": 3, + "maxLength": 8, + "description": "UOM" + }, + "UnitPrice": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.999, + "description": "Rate" + }, + "TotAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Gross Amount" + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Discount" + }, + "PreTaxVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Pre tax value" + }, + "AssAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Taxable Value" + }, + "GstRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999, + "description": "GST Rate" + }, + "IgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "IGST Amount" + }, + "CgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "CGST Amount" + }, + "SgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "SGST Amount" + }, + "CesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999, + "description": "Cess Rate" + }, + "CesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Cess Amount (Advalorem)" + }, + "CesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Cess Amount (Non-Advalorem)" + }, + "StateCesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999, + "description": "State CESS Rate" + }, + "StateCesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "State CESS Amount" + }, + "StateCesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "State CESS Amount (Non Advalorem)" + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Other Charges" + }, + "TotItemVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Total Item Value" + }, + "OrdLineRef": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "description": "Order line reference" + }, + "OrgCntry": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "Origin Country" + }, + "PrdSlNo": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "Serial number" + }, + "BchDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 20, + "description": "Batch number" + }, + "ExpDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Batch Expiry Date" + }, + "WrDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Warranty Date" + } + }, + "required": ["Nm"] + }, + "AttribDtls": { + "type": "Array", + "Attribute": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Attribute name of the item" + }, + "Val": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Attribute value of the item" + } + } + } + } + }, + "required": [ + "SlNo", + "IsServc", + "HsnCd", + "UnitPrice", + "TotAmt", + "AssAmt", + "GstRt", + "TotItemVal" + ] + }, + "ValDtls": { + "type": "object", + "properties": { + "AssVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total Assessable value of all items" + }, + "CgstVal": { + "type": "number", + "maximum": 99999999999999.99, + "minimum": 0, + "description": "Total CGST value of all items" + }, + "SgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total SGST value of all items" + }, + "IgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total IGST value of all items" + }, + "CesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total CESS value of all items" + }, + "StCesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total State CESS value of all items" + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Invoice Discount" + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Other Charges" + }, + "RndOffAmt": { + "type": "number", + "minimum": -99.99, + "maximum": 99.99, + "description": "Rounded off Amount" + }, + "TotInvVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Final Invoice Value " + }, + "TotInvValFc": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Final Invoice value in Foreign Currency" + } + }, + "required": ["AssVal", "TotInvVal"] + }, + "PayDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Payee Name" + }, + "AccDet": { + "type": "string", + "minLength": 1, + "maxLength": 18, + "description": "Bank Account Number of Payee" + }, + "Mode": { + "type": "string", + "minLength": 1, + "maxLength": 18, + "description": "Mode of Payment" + }, + "FinInsBr": { + "type": "string", + "minLength": 1, + "maxLength": 11, + "description": "Branch or IFSC code" + }, + "PayTerm": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Terms of Payment" + }, + "PayInstr": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Payment Instruction" + }, + "CrTrn": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Credit Transfer" + }, + "DirDr": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Direct Debit" + }, + "CrDay": { + "type": "number", + "minimum": 0, + "maximum": 9999, + "description": "Credit Days" + }, + "PaidAmt": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Advance Amount" + }, + "PaymtDue": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Outstanding Amount" + } + } + }, + "RefDtls": { + "type": "object", + "properties": { + "InvRm": { + "type": "string", + "maxLength": 100, + "minLength": 3, + "pattern": "^[0-9A-Za-z/-]{3,100}$", + "description": "Remarks/Note" + }, + "DocPerdDtls": { + "type": "object", + "properties": { + "InvStDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Invoice Period Start Date" + }, + "InvEndDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Invoice Period End Date" + } + }, + "required": ["InvStDt ", "InvEndDt "] + }, + "PrecDocDtls": { + "type": "object", + "properties": { + "InvNo": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$", + "description": "Reference of Original Invoice" + }, + "InvDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Date of Orginal Invoice" + }, + "OthRefNo": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "Other Reference" + } + } + }, + "required": ["InvNo", "InvDt"], + "ContrDtls": { + "type": "object", + "properties": { + "RecAdvRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Receipt Advice No." + }, + "RecAdvDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Date of receipt advice" + }, + "TendRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Lot/Batch Reference No." + }, + "ContrRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Contract Reference Number" + }, + "ExtRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Any other reference" + }, + "ProjRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Project Reference Number" + }, + "PORefr": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([0-9A-Za-z/-]){1,16}$", + "description": "PO Reference Number" + }, + "PORefDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "PO Reference date" + } + } + } + } + }, + "AddlDocDtls": { + "type": "Array", + "properties": { + "Url": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Supporting document URL" + }, + "Docs": { + "type": "string", + "minLength": 3, + "maxLength": 1000, + "description": "Supporting document in Base64 Format" + }, + "Info": { + "type": "string", + "minLength": 3, + "maxLength": 1000, + "description": "Any additional information" + } + } + }, + + "ExpDtls": { + "type": "object", + "properties": { + "ShipBNo": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "Shipping Bill No." + }, + "ShipBDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Shipping Bill Date" + }, + "Port": { + "type": "string", + "minLength": 2, + "maxLength": 10, + "pattern": "^[0-9A-Za-z]{2,10}$", + "description": "Port Code. Refer the master" + }, + "RefClm": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "description": "Claiming Refund. Y/N" + }, + "ForCur": { + "type": "string", + "minLength": 3, + "maxLength": 16, + "description": "Additional Currency Code. Refer the master" + }, + "CntCode": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "Country Code. Refer the master" + }, + "ExpDuty": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Export Duty" + } + } + }, + "EwbDtls": { + "type": "object", + "properties": { + "TransId": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "description": "Transporter GSTIN" + }, + "TransName": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Transporter Name" + }, + "TransMode": { + "type": "string", + "maxLength": 1, + "minLength": 1, + "enum": ["1", "2", "3", "4"], + "description": "Mode of Transport" + }, + "Distance": { + "type": "number", + "minimum": 1, + "maximum": 9999, + "description": "Distance" + }, + "TransDocNo": { + "type": "string", + "minLength": 1, + "maxLength": 15, + "pattern": "^([0-9A-Z/-]){1,15}$", + "description": "Tranport Document Number", + "validationMsg": "Transport Receipt No is invalid" + }, + "TransDocDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Transport Document Date" + }, + "VehNo": { + "type": "string", + "minLength": 4, + "maxLength": 20, + "description": "Vehicle Number" + }, + "VehType": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["O", "R"], + "description": "Vehicle Type" + } + }, + "required": ["Distance"] + }, + "required": [ + "Version", + "TranDtls", + "DocDtls", + "SellerDtls", + "BuyerDtls", + "ItemList", + "ValDtls" + ] +} diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js new file mode 100644 index 00000000000..348f0c6feed --- /dev/null +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -0,0 +1,292 @@ +erpnext.setup_einvoice_actions = (doctype) => { + frappe.ui.form.on(doctype, { + async refresh(frm) { + if (frm.doc.docstatus == 2) return; + + const res = await frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility', + args: { doc: frm.doc } + }); + const invoice_eligible = res.message; + + if (!invoice_eligible) return; + + const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + + const add_custom_button = (label, action) => { + if (!frm.custom_buttons[label]) { + frm.add_custom_button(label, action, __('E Invoicing')); + } + }; + + if (!irn && !__unsaved) { + const action = () => { + if (frm.doc.__unsaved) { + frappe.throw(__('Please save the document to generate IRN.')); + } + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.get_einvoice', + args: { doctype, docname: name }, + freeze: true, + callback: (res) => { + const einvoice = res.message; + show_einvoice_preview(frm, einvoice); + } + }); + }; + + add_custom_button(__("Generate IRN"), action); + } + + if (irn && !irn_cancelled && !ewaybill) { + const fields = [ + { + "label": "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; + const action = () => { + const d = new frappe.ui.Dialog({ + title: __("Cancel IRN"), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_irn', + args: { + doctype, + docname: name, + irn: irn, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + add_custom_button(__("Cancel IRN"), action); + } + + if (irn && !irn_cancelled && !ewaybill) { + const action = () => { + const d = new frappe.ui.Dialog({ + title: __('Generate E-Way Bill'), + size: "large", + fields: get_ewaybill_fields(frm), + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill', + args: { + doctype, + docname: name, + irn, + ...data + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + + add_custom_button(__("Generate E-Way Bill"), action); + } + + if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { + const action = () => { + let message = __('Cancellation of e-way bill is currently not supported.') + ' '; + message += '

'; + message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); + + const dialog = frappe.msgprint({ + title: __('Update E-Way Bill Cancelled Status?'), + message: message, + indicator: 'orange', + primary_action: { + action: function() { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', + args: { doctype, docname: name }, + freeze: true, + callback: () => frm.reload_doc() || dialog.hide() + }); + } + }, + primary_action_label: __('Yes') + }); + }; + add_custom_button(__("Cancel E-Way Bill"), action); + } + } + }); +}; + +const get_ewaybill_fields = (frm) => { + return [ + { + 'fieldname': 'transporter', + 'label': 'Transporter', + 'fieldtype': 'Link', + 'options': 'Supplier', + 'default': frm.doc.transporter + }, + { + 'fieldname': 'gst_transporter_id', + 'label': 'GST Transporter ID', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.gst_transporter_id', + 'default': frm.doc.gst_transporter_id + }, + { + 'fieldname': 'driver', + 'label': 'Driver', + 'fieldtype': 'Link', + 'options': 'Driver', + 'default': frm.doc.driver + }, + { + 'fieldname': 'lr_no', + 'label': 'Transport Receipt No', + 'fieldtype': 'Data', + 'default': frm.doc.lr_no + }, + { + 'fieldname': 'vehicle_no', + 'label': 'Vehicle No', + 'fieldtype': 'Data', + 'default': frm.doc.vehicle_no + }, + { + 'fieldname': 'distance', + 'label': 'Distance (in km)', + 'fieldtype': 'Float', + 'default': frm.doc.distance + }, + { + 'fieldname': 'transporter_col_break', + 'fieldtype': 'Column Break', + }, + { + 'fieldname': 'transporter_name', + 'label': 'Transporter Name', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.name', + 'read_only': 1, + 'default': frm.doc.transporter_name + }, + { + 'fieldname': 'mode_of_transport', + 'label': 'Mode of Transport', + 'fieldtype': 'Select', + 'options': `\nRoad\nAir\nRail\nShip`, + 'default': frm.doc.mode_of_transport + }, + { + 'fieldname': 'driver_name', + 'label': 'Driver Name', + 'fieldtype': 'Data', + 'fetch_from': 'driver.full_name', + 'read_only': 1, + 'default': frm.doc.driver_name + }, + { + 'fieldname': 'lr_date', + 'label': 'Transport Receipt Date', + 'fieldtype': 'Date', + 'default': frm.doc.lr_date + }, + { + 'fieldname': 'gst_vehicle_type', + 'label': 'GST Vehicle Type', + 'fieldtype': 'Select', + 'options': `Regular\nOver Dimensional Cargo (ODC)`, + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', + 'default': frm.doc.gst_vehicle_type + } + ]; +}; + +const request_irn_generation = (frm) => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_irn', + args: { doctype: frm.doc.doctype, docname: frm.doc.name }, + freeze: true, + callback: () => frm.reload_doc() + }); +}; + +const get_preview_dialog = (frm, action) => { + const dialog = new frappe.ui.Dialog({ + title: __("Preview"), + size: "large", + fields: [ + { + "label": "Preview", + "fieldname": "preview_html", + "fieldtype": "HTML" + } + ], + primary_action: () => action(frm) || dialog.hide(), + primary_action_label: __('Generate IRN') + }); + return dialog; +}; + +const show_einvoice_preview = (frm, einvoice) => { + const preview_dialog = get_preview_dialog(frm, request_irn_generation); + + // initialize e-invoice fields + einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate(); + frm.doc.signed_einvoice = JSON.stringify(einvoice); + + // initialize preview wrapper + const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper; + $preview_wrapper.html( + `
+ +
+
` + ); + + frappe.call({ + method: "frappe.www.printview.get_html_and_style", + args: { + doc: frm.doc, + print_format: "GST E-Invoice", + no_letterhead: 1 + }, + callback: function (r) { + if (!r.exc) { + $preview_wrapper.find(".print-format").html(r.message.html); + const style = ` + .print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; } + .print-preview { min-height: 0px; } + .modal-dialog { width: 720px; }`; + + frappe.dom.set_style(style, "custom-print-style"); + preview_dialog.show(); + } + } + }); +}; diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py new file mode 100644 index 00000000000..e3f7e90ff3c --- /dev/null +++ b/erpnext/regional/india/e_invoice/utils.py @@ -0,0 +1,1171 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import base64 +import io +import json +import os +import re +import sys +import traceback + +import frappe +import jwt +from frappe import _, bold +from frappe.core.page.background_jobs.background_jobs import get_info +from frappe.integrations.utils import make_get_request, make_post_request +from frappe.utils.background_jobs import enqueue +from frappe.utils.data import ( + add_to_date, + cint, + cstr, + flt, + format_date, + get_link_to_form, + getdate, + now_datetime, + time_diff_in_hours, + time_diff_in_seconds, +) +from frappe.utils.scheduler import is_scheduler_inactive +from pyqrcode import create as qrcreate + +from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply + + +@frappe.whitelist() +def validate_eligibility(doc): + if isinstance(doc, str): + doc = json.loads(doc) + + invalid_doctype = doc.get('doctype') != 'Sales Invoice' + if invalid_doctype: + return False + + einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable')) + if not einvoicing_enabled: + return False + + einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01' + if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): + return False + + invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) + invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] + company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') + + # if export invoice, then taxes can be empty + # invoice can only be ineligible if no taxes applied and is not an export invoice + no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas' + has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst')) + + if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item: + return False + + return True + +def validate_einvoice_fields(doc): + invoice_eligible = validate_eligibility(doc) + + if not invoice_eligible: + return + + if doc.docstatus == 0 and doc._action == 'save': + if doc.irn: + frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) + if len(doc.name) > 16: + raise_document_name_too_long_error() + + doc.einvoice_status = 'Pending' + + elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: + frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) + + elif doc.irn and doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: + frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) + +def raise_document_name_too_long_error(): + title = _('Document ID Too Long') + msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice') + msg += ', ' + msg += _('document id {} exceed 16 letters.').format(bold(_('should not'))) + msg += '

' + msg += _('You must {} your {} in order to have document id of {} length 16.').format( + bold(_('modify')), bold(_('naming series')), bold(_('maximum')) + ) + msg += _('Please account for ammended documents too.') + frappe.throw(msg, title=title) + +def read_json(name): + file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name)) + with open(file_path, 'r') as f: + return cstr(f.read()) + +def get_transaction_details(invoice): + supply_type = '' + if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' + elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' + elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' + elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' + + if not supply_type: + rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export') + frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export), + title=_('Invalid Supply Type')) + + return frappe._dict(dict( + tax_scheme='GST', + supply_type=supply_type, + reverse_charge=invoice.reverse_charge + )) + +def get_doc_details(invoice): + if getdate(invoice.posting_date) < getdate('2021-01-01'): + frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed')) + + invoice_type = 'CRN' if invoice.is_return else 'INV' + + invoice_name = invoice.name + invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') + + return frappe._dict(dict( + invoice_type=invoice_type, + invoice_name=invoice_name, + invoice_date=invoice_date + )) + +def validate_address_fields(address, skip_gstin_validation): + if ((not address.gstin and not skip_gstin_validation) + or not address.city + or not address.pincode + or not address.address_title + or not address.address_line1 + or not address.gst_state_number): + + frappe.throw( + msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name), + title=_('Missing Address Fields') + ) + + if address.address_line2 and len(address.address_line2) < 2: + # to prevent "The field Address 2 must be a string with a minimum length of 3 and a maximum length of 100" + address.address_line2 = "" + +def get_party_details(address_name, skip_gstin_validation=False): + addr = frappe.get_doc('Address', address_name) + + validate_address_fields(addr, skip_gstin_validation) + + if addr.gst_state_number == 97: + # according to einvoice standard + addr.pincode = 999999 + + party_address_details = frappe._dict(dict( + legal_name=sanitize_for_json(addr.address_title), + location=sanitize_for_json(addr.city), + pincode=addr.pincode, gstin=addr.gstin, + state_code=addr.gst_state_number, + address_line1=sanitize_for_json(addr.address_line1), + address_line2=sanitize_for_json(addr.address_line2) + )) + + return party_address_details + +def get_overseas_address_details(address_name): + address_title, address_line1, address_line2, city = frappe.db.get_value( + 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city'] + ) + + if not address_title or not address_line1 or not city: + frappe.throw( + msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format( + get_link_to_form('Address', address_name) + ), + title=_('Missing Address Fields') + ) + + return frappe._dict(dict( + gstin='URP', + legal_name=sanitize_for_json(address_title), + location=city, + address_line1=sanitize_for_json(address_line1), + address_line2=sanitize_for_json(address_line2), + pincode=999999, state_code=96, place_of_supply=96 + )) + +def get_item_list(invoice): + item_list = [] + + for d in invoice.items: + einvoice_item_schema = read_json('einv_item_template') + item = frappe._dict({}) + item.update(d.as_dict()) + + item.sr_no = d.idx + item.description = sanitize_for_json(d.item_name) + + item.qty = abs(item.qty) + if flt(item.qty) != 0.0: + item.unit_rate = abs(item.taxable_value / item.qty) + else: + item.unit_rate = abs(item.taxable_value) + item.gross_amount = abs(item.taxable_value) + item.taxable_value = abs(item.taxable_value) + item.discount_amount = 0 + + item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None + item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None + item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N' + item.serial_no = "" + + item = update_item_taxes(invoice, item) + + item.total_value = abs( + item.taxable_value + item.igst_amount + item.sgst_amount + + item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges + ) + einv_item = einvoice_item_schema.format(item=item) + item_list.append(einv_item) + + return ', '.join(item_list) + +def update_item_taxes(invoice, item): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + for attr in [ + 'tax_rate', 'cess_rate', 'cess_nadv_amount', + 'cgst_amount', 'sgst_amount', 'igst_amount', + 'cess_amount', 'cess_nadv_amount', 'other_charges' + ]: + item[attr] = 0 + + for t in invoice.taxes: + is_applicable = t.tax_amount and t.account_head in gst_accounts_list + if is_applicable: + # this contains item wise tax rate & tax amount (incl. discount) + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name) + + item_tax_rate = item_tax_detail[0] + # item tax amount excluding discount amount + item_tax_amount = (item_tax_rate / 100) * item.taxable_value + + if t.account_head in gst_accounts.cess_account: + item_tax_amount_after_discount = item_tax_detail[1] + if t.charge_type == 'On Item Quantity': + item.cess_nadv_amount += abs(item_tax_amount_after_discount) + else: + item.cess_rate += item_tax_rate + item.cess_amount += abs(item_tax_amount_after_discount) + + for tax_type in ['igst', 'cgst', 'sgst']: + if t.account_head in gst_accounts[f'{tax_type}_account']: + item.tax_rate += item_tax_rate + item[f'{tax_type}_amount'] += abs(item_tax_amount) + else: + # TODO: other charges per item + pass + + return item + +def get_invoice_value_details(invoice): + invoice_value_details = frappe._dict(dict()) + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) + invoice_value_details.invoice_discount_amt = 0 + + invoice_value_details.round_off = invoice.base_rounding_adjustment + invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) + invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) + + invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) + + return invoice_value_details + +def update_invoice_taxes(invoice, invoice_value_details): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + invoice_value_details.total_cgst_amt = 0 + invoice_value_details.total_sgst_amt = 0 + invoice_value_details.total_igst_amt = 0 + invoice_value_details.total_cess_amt = 0 + invoice_value_details.total_other_charges = 0 + considered_rows = [] + + for t in invoice.taxes: + tax_amount = t.base_tax_amount_after_discount_amount + if t.account_head in gst_accounts_list: + if t.account_head in gst_accounts.cess_account: + # using after discount amt since item also uses after discount amt for cess calc + invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) + + for tax_type in ['igst', 'cgst', 'sgst']: + if t.account_head in gst_accounts[f'{tax_type}_account']: + + invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount) + update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows) + else: + invoice_value_details.total_other_charges += abs(tax_amount) + + return invoice_value_details + +def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows): + prev_row_id = cint(tax_row.row_id) - 1 + if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows: + if tax_row.charge_type == 'On Previous Row Amount': + amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount + invoice_value_details.total_other_charges -= abs(amount) + considered_rows.append(prev_row_id) + if tax_row.charge_type == 'On Previous Row Total': + amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total + invoice_value_details.total_other_charges -= abs(amount) + considered_rows.append(prev_row_id) + +def get_payment_details(invoice): + payee_name = invoice.company + mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) + paid_amount = invoice.base_paid_amount + outstanding_amount = invoice.outstanding_amount + + return frappe._dict(dict( + payee_name=payee_name, mode_of_payment=mode_of_payment, + paid_amount=paid_amount, outstanding_amount=outstanding_amount + )) + +def get_return_doc_reference(invoice): + invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') + return frappe._dict(dict( + invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') + )) + +def get_eway_bill_details(invoice): + if invoice.is_return: + frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'), + title=_('Invalid Fields')) + + + mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } + vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } + + return frappe._dict(dict( + gstin=invoice.gst_transporter_id, + name=invoice.transporter_name, + mode_of_transport=mode_of_transport[invoice.mode_of_transport], + distance=invoice.distance or 0, + document_name=invoice.lr_no, + document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'), + vehicle_no=invoice.vehicle_no, + vehicle_type=vehicle_type[invoice.gst_vehicle_type] + )) + +def validate_mandatory_fields(invoice): + if not invoice.company_address: + frappe.throw( + _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'), + title=_('Missing Fields') + ) + if not invoice.customer_address: + frappe.throw( + _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'), + title=_('Missing Fields') + ) + if not frappe.db.get_value('Address', invoice.company_address, 'gstin'): + frappe.throw( + _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), + title=_('Missing Fields') + ) + if invoice.gst_category != 'Overseas' and not frappe.db.get_value('Address', invoice.customer_address, 'gstin'): + frappe.throw( + _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'), + title=_('Missing Fields') + ) + +def validate_totals(einvoice): + item_list = einvoice['ItemList'] + value_details = einvoice['ValDtls'] + + total_item_ass_value = 0 + total_item_cgst_value = 0 + total_item_sgst_value = 0 + total_item_igst_value = 0 + total_item_value = 0 + for item in item_list: + total_item_ass_value += flt(item['AssAmt']) + total_item_cgst_value += flt(item['CgstAmt']) + total_item_sgst_value += flt(item['SgstAmt']) + total_item_igst_value += flt(item['IgstAmt']) + total_item_value += flt(item['TotItemVal']) + + if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1: + frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx)) + + if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1: + frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.')) + + if abs(flt(value_details['CgstVal']) + flt(value_details['SgstVal']) - total_item_cgst_value - total_item_sgst_value) > 1: + frappe.throw(_('CGST + SGST value of the items is not equal to total CGST + SGST value. Please review taxes for any correction.')) + + if abs(flt(value_details['IgstVal']) - total_item_igst_value) > 1: + frappe.throw(_('IGST value of all items is not equal to total IGST value. Please review taxes for any correction.')) + + if abs( + flt(value_details['TotInvVal']) + flt(value_details['Discount']) - + flt(value_details['OthChrg']) - flt(value_details['RndOffAmt']) - + total_item_value + ) > 1: + frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.')) + + calculated_invoice_value = \ + flt(value_details['AssVal']) + flt(value_details['CgstVal']) \ + + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \ + + flt(value_details['OthChrg']) + flt(value_details['RndOffAmt']) - flt(value_details['Discount']) + + if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1: + frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.')) + +def make_einvoice(invoice): + validate_mandatory_fields(invoice) + + schema = read_json('einv_template') + + transaction_details = get_transaction_details(invoice) + item_list = get_item_list(invoice) + doc_details = get_doc_details(invoice) + invoice_value_details = get_invoice_value_details(invoice) + seller_details = get_party_details(invoice.company_address) + + if invoice.gst_category == 'Overseas': + buyer_details = get_overseas_address_details(invoice.customer_address) + else: + buyer_details = get_party_details(invoice.customer_address) + place_of_supply = get_place_of_supply(invoice, invoice.doctype) + if place_of_supply: + place_of_supply = place_of_supply.split('-')[0] + else: + place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2] + buyer_details.update(dict(place_of_supply=place_of_supply)) + + seller_details.update(dict(legal_name=invoice.company)) + buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer)) + + shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) + if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: + if invoice.gst_category == 'Overseas': + shipping_details = get_overseas_address_details(invoice.shipping_address_name) + else: + shipping_details = get_party_details(invoice.shipping_address_name, skip_gstin_validation=True) + + dispatch_details = frappe._dict({}) + if invoice.dispatch_address_name: + dispatch_details = get_party_details(invoice.dispatch_address_name, skip_gstin_validation=True) + + if invoice.is_pos and invoice.base_paid_amount: + payment_details = get_payment_details(invoice) + + if invoice.is_return and invoice.return_against: + prev_doc_details = get_return_doc_reference(invoice) + + if invoice.transporter and not invoice.is_return: + eway_bill_details = get_eway_bill_details(invoice) + + # not yet implemented + period_details = export_details = frappe._dict({}) + + einvoice = schema.format( + transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details, + seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, + item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details, + period_details=period_details, prev_doc_details=prev_doc_details, + export_details=export_details, eway_bill_details=eway_bill_details + ) + + try: + einvoice = safe_json_load(einvoice) + einvoice = santize_einvoice_fields(einvoice) + except Exception: + show_link_to_error_log(invoice, einvoice) + + try: + validate_totals(einvoice) + except Exception: + log_error(einvoice) + raise + + return einvoice + +def show_link_to_error_log(invoice, einvoice): + err_log = log_error(einvoice) + link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log') + frappe.throw( + _('An error occurred while creating e-invoice for {}. Please check {} for more information.').format( + invoice.name, link_to_error_log), + title=_('E Invoice Creation Failed') + ) + +def log_error(data=None): + if isinstance(data, str): + data = json.loads(data) + + seperator = "--" * 50 + err_tb = traceback.format_exc() + err_msg = str(sys.exc_info()[1]) + data = json.dumps(data, indent=4) + + message = "\n".join([ + "Error", err_msg, seperator, + "Data:", data, seperator, + "Exception:", err_tb + ]) + return frappe.log_error(title=_('E Invoice Request Failed'), message=message) + +def santize_einvoice_fields(einvoice): + int_fields = ["Pin","Distance","CrDay"] + float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",] + copy = einvoice.copy() + for key, value in copy.items(): + if isinstance(value, list): + for idx, d in enumerate(value): + santized_dict = santize_einvoice_fields(d) + if santized_dict: + einvoice[key][idx] = santized_dict + else: + einvoice[key].pop(idx) + + if not einvoice[key]: + einvoice.pop(key, None) + + elif isinstance(value, dict): + santized_dict = santize_einvoice_fields(value) + if santized_dict: + einvoice[key] = santized_dict + else: + einvoice.pop(key, None) + + elif not value or value == "None": + einvoice.pop(key, None) + + elif key in float_fields: + einvoice[key] = flt(value, 2) + + elif key in int_fields: + einvoice[key] = cint(value) + + return einvoice + +def safe_json_load(json_string): + try: + return json.loads(json_string) + except json.JSONDecodeError as e: + # print a snippet of 40 characters around the location where error occured + pos = e.pos + start, end = max(0, pos-20), min(len(json_string)-1, pos+20) + snippet = json_string[start:end] + frappe.throw(_("Error in input data. Please check for any special characters near following input:
{}").format(snippet)) + +class RequestFailed(Exception): + pass +class CancellationNotAllowed(Exception): + pass + +class GSPConnector(): + def __init__(self, doctype=None, docname=None): + self.doctype = doctype + self.docname = docname + + self.set_invoice() + self.set_credentials() + + # authenticate url is same for sandbox & live + self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token' + self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test' + + self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' + self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' + self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' + self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' + self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB' + self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' + + def set_invoice(self): + self.invoice = None + if self.doctype and self.docname: + self.invoice = frappe.get_cached_doc(self.doctype, self.docname) + + def set_credentials(self): + self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') + + if not self.e_invoice_settings.enable: + frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings"))) + + if self.invoice: + gstin = self.get_seller_gstin() + credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin] + if credentials_for_gstin: + self.credentials = credentials_for_gstin[0] + else: + frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings')) + else: + self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None + + def get_seller_gstin(self): + gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin') + if not gstin: + frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) + return gstin + + def get_auth_token(self): + if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0: + self.fetch_auth_token() + + return self.e_invoice_settings.auth_token + + def make_request(self, request_type, url, headers=None, data=None): + if request_type == 'post': + res = make_post_request(url, headers=headers, data=data) + else: + res = make_get_request(url, headers=headers, data=data) + + self.log_request(url, headers, data, res) + return res + + def log_request(self, url, headers, data, res): + headers.update({ 'password': self.credentials.password }) + request_log = frappe.get_doc({ + "doctype": "E Invoice Request Log", + "user": frappe.session.user, + "reference_invoice": self.invoice.name if self.invoice else None, + "url": url, + "headers": json.dumps(headers, indent=4) if headers else None, + "data": json.dumps(data, indent=4) if isinstance(data, dict) else data, + "response": json.dumps(res, indent=4) if res else None + }) + request_log.save(ignore_permissions=True) + frappe.db.commit() + + def get_client_credentials(self): + if self.e_invoice_settings.client_id and self.e_invoice_settings.client_secret: + return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password('client_secret') + + return frappe.conf.einvoice_client_id, frappe.conf.einvoice_client_secret + + def fetch_auth_token(self): + client_id, client_secret = self.get_client_credentials() + headers = { + 'gspappid': client_id, + 'gspappsecret': client_secret + } + res = {} + try: + res = self.make_request('post', self.authenticate_url, headers) + self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) + self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in')) + self.e_invoice_settings.save(ignore_permissions=True) + self.e_invoice_settings.reload() + + except Exception: + log_error(res) + self.raise_error(True) + + def get_headers(self): + return { + 'content-type': 'application/json', + 'user_name': self.credentials.username, + 'password': self.credentials.get_password(), + 'gstin': self.credentials.gstin, + 'authorization': self.get_auth_token(), + 'requestid': str(base64.b64encode(os.urandom(18))), + } + + def fetch_gstin_details(self, gstin): + headers = self.get_headers() + + try: + params = '?gstin={gstin}'.format(gstin=gstin) + res = self.make_request('get', self.gstin_details_url + params, headers) + if res.get('success'): + return res.get('result') + else: + log_error(res) + raise RequestFailed + + except RequestFailed: + self.raise_error() + + except Exception: + log_error() + self.raise_error(True) + @staticmethod + def get_gstin_details(gstin): + '''fetch and cache GSTIN details''' + if not hasattr(frappe.local, 'gstin_cache'): + frappe.local.gstin_cache = {} + + key = gstin + gsp_connector = GSPConnector() + details = gsp_connector.fetch_gstin_details(gstin) + + frappe.local.gstin_cache[key] = details + frappe.cache().hset('gstin_cache', key, details) + return details + + def generate_irn(self): + data = {} + try: + headers = self.get_headers() + einvoice = make_einvoice(self.invoice) + data = json.dumps(einvoice, indent=4) + res = self.make_request('post', self.generate_irn_url, headers, data) + + if res.get('success'): + self.set_einvoice_data(res.get('result')) + + elif '2150' in res.get('message'): + # IRN already generated but not updated in invoice + # Extract the IRN from the response description and fetch irn details + irn = res.get('result')[0].get('Desc').get('Irn') + irn_details = self.get_irn_details(irn) + if irn_details: + self.set_einvoice_data(irn_details) + else: + raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \ + Contact ERPNext support to resolve the issue.') + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.set_failed_status(errors=errors) + self.raise_error(errors=errors) + + except Exception as e: + self.set_failed_status(errors=str(e)) + log_error(data) + self.raise_error(True) + + @staticmethod + def bulk_generate_irn(invoices): + gsp_connector = GSPConnector() + gsp_connector.doctype = 'Sales Invoice' + + failed = [] + + for invoice in invoices: + try: + gsp_connector.docname = invoice + gsp_connector.set_invoice() + gsp_connector.set_credentials() + gsp_connector.generate_irn() + + except Exception as e: + failed.append({ + 'docname': invoice, + 'message': str(e) + }) + + return failed + + def get_irn_details(self, irn): + headers = self.get_headers() + + try: + params = '?irn={irn}'.format(irn=irn) + res = self.make_request('get', self.irn_details_url + params, headers) + if res.get('success'): + return res.get('result') + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + log_error() + self.raise_error(True) + + def cancel_irn(self, irn, reason, remark): + data, res = {}, {} + try: + # validate cancellation + if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24: + frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed) + if not irn: + frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed) + + headers = self.get_headers() + data = json.dumps({ + 'Irn': irn, + 'Cnlrsn': reason, + 'Cnlrem': remark + }, indent=4) + + res = self.make_request('post', self.cancel_irn_url, headers, data) + if res.get('success') or '9999' in res.get('message'): + self.invoice.irn_cancelled = 1 + self.invoice.irn_cancel_date = res.get('result')['CancelDate'] if res.get('result') else "" + self.invoice.einvoice_status = 'Cancelled' + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('IRN Cancelled - {}').format(remark) + } + self.update_invoice() + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.set_failed_status(errors=errors) + self.raise_error(errors=errors) + + except CancellationNotAllowed as e: + self.set_failed_status(errors=str(e)) + self.raise_error(errors=str(e)) + + except Exception as e: + self.set_failed_status(errors=str(e)) + log_error(data) + self.raise_error(True) + + @staticmethod + def bulk_cancel_irn(invoices, reason, remark): + gsp_connector = GSPConnector() + gsp_connector.doctype = 'Sales Invoice' + + failed = [] + + for invoice in invoices: + try: + gsp_connector.docname = invoice + gsp_connector.set_invoice() + gsp_connector.set_credentials() + irn = gsp_connector.invoice.irn + gsp_connector.cancel_irn(irn, reason, remark) + + except Exception as e: + failed.append({ + 'docname': invoice, + 'message': str(e) + }) + + return failed + + def generate_eway_bill(self, **kwargs): + args = frappe._dict(kwargs) + + headers = self.get_headers() + eway_bill_details = get_eway_bill_details(args) + data = json.dumps({ + 'Irn': args.irn, + 'Distance': cint(eway_bill_details.distance), + 'TransMode': eway_bill_details.mode_of_transport, + 'TransId': eway_bill_details.gstin, + 'TransName': eway_bill_details.transporter, + 'TrnDocDt': eway_bill_details.document_date, + 'TrnDocNo': eway_bill_details.document_name, + 'VehNo': eway_bill_details.vehicle_no, + 'VehType': eway_bill_details.vehicle_type + }, indent=4) + + try: + res = self.make_request('post', self.generate_ewaybill_url, headers, data) + if res.get('success'): + self.invoice.ewaybill = res.get('result').get('EwbNo') + self.invoice.eway_bill_validity = res.get('result').get('EwbValidTill') + self.invoice.eway_bill_cancelled = 0 + self.invoice.update(args) + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('E-Way Bill Generated') + } + self.update_invoice() + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + log_error(data) + self.raise_error(True) + + def cancel_eway_bill(self, eway_bill, reason, remark): + headers = self.get_headers() + data = json.dumps({ + 'ewbNo': eway_bill, + 'cancelRsnCode': reason, + 'cancelRmrk': remark + }, indent=4) + headers["username"] = headers["user_name"] + del headers["user_name"] + try: + res = self.make_request('post', self.cancel_ewaybill_url, headers, data) + if res.get('success'): + self.invoice.ewaybill = '' + self.invoice.eway_bill_cancelled = 1 + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('E-Way Bill Cancelled - {}').format(remark) + } + self.update_invoice() + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + log_error(data) + self.raise_error(True) + + def sanitize_error_message(self, message): + ''' + On validation errors, response message looks something like this: + message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, + 3095 : Supplier GSTIN is inactive' + we search for string between ':' to extract the error messages + errors = [ + ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', + ': Test' + ] + then we trim down the message by looping over errors + ''' + if not message: + return [] + + errors = re.findall(': [^:]+', message) + for idx, e in enumerate(errors): + # remove colons + errors[idx] = errors[idx].replace(':', '').strip() + # if not last + if idx != len(errors) - 1: + # remove last 7 chars eg: ', 3095 ' + errors[idx] = errors[idx][:-6] + + return errors + + def raise_error(self, raise_exception=False, errors=None): + if errors is None: + errors = [] + title = _('E Invoice Request Failed') + if errors: + frappe.throw(errors, title=title, as_list=1) + else: + link_to_error_list = 'Error Log' + frappe.msgprint( + _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list), + title=title, + raise_exception=raise_exception, + indicator='red' + ) + + def set_einvoice_data(self, res): + enc_signed_invoice = res.get('SignedInvoice') + dec_signed_invoice = jwt.decode(enc_signed_invoice, options={"verify_signature": False})['data'] + + self.invoice.irn = res.get('Irn') + self.invoice.ewaybill = res.get('EwbNo') + self.invoice.eway_bill_validity = res.get('EwbValidTill') + self.invoice.ack_no = res.get('AckNo') + self.invoice.ack_date = res.get('AckDt') + self.invoice.signed_einvoice = dec_signed_invoice + self.invoice.ack_no = res.get('AckNo') + self.invoice.ack_date = res.get('AckDt') + self.invoice.signed_qr_code = res.get('SignedQRCode') + self.invoice.einvoice_status = 'Generated' + + self.attach_qrcode_image() + + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('IRN Generated') + } + self.update_invoice() + + def attach_qrcode_image(self): + qrcode = self.invoice.signed_qr_code + doctype = self.invoice.doctype + docname = self.invoice.name + filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__") + + qr_image = io.BytesIO() + url = qrcreate(qrcode, error='L') + url.png(qr_image, scale=2, quiet_zone=1) + _file = frappe.get_doc({ + "doctype": "File", + "file_name": filename, + "attached_to_doctype": doctype, + "attached_to_name": docname, + "attached_to_field": "qrcode_image", + "is_private": 0, + "content": qr_image.getvalue()}) + _file.save() + frappe.db.commit() + self.invoice.qrcode_image = _file.file_url + + def update_invoice(self): + self.invoice.flags.ignore_validate_update_after_submit = True + self.invoice.flags.ignore_validate = True + self.invoice.save() + + def set_failed_status(self, errors=None): + frappe.db.rollback() + self.invoice.einvoice_status = 'Failed' + self.invoice.failure_description = self.get_failure_message(errors) if errors else "" + self.update_invoice() + frappe.db.commit() + + def get_failure_message(self, errors): + if isinstance(errors, list): + errors = ', '.join(errors) + return errors + +def sanitize_for_json(string): + """Escape JSON specific characters from a string.""" + + # json.dumps adds double-quotes to the string. Indexing to remove them. + return json.dumps(string)[1:-1] + +@frappe.whitelist() +def get_einvoice(doctype, docname): + invoice = frappe.get_doc(doctype, docname) + return make_einvoice(invoice) + +@frappe.whitelist() +def generate_irn(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.generate_irn() + +@frappe.whitelist() +def cancel_irn(doctype, docname, irn, reason, remark): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_irn(irn, reason, remark) + +@frappe.whitelist() +def generate_eway_bill(doctype, docname, **kwargs): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.generate_eway_bill(**kwargs) + +@frappe.whitelist() +def cancel_eway_bill(doctype, docname): + # TODO: uncomment when eway_bill api from Adequare is enabled + # gsp_connector = GSPConnector(doctype, docname) + # gsp_connector.cancel_eway_bill(eway_bill, reason, remark) + + frappe.db.set_value(doctype, docname, 'ewaybill', '') + frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) + +@frappe.whitelist() +def generate_einvoices(docnames): + docnames = json.loads(docnames) or [] + + if len(docnames) < 10: + failures = GSPConnector.bulk_generate_irn(docnames) + frappe.local.message_log = [] + + if failures: + show_bulk_action_failure_message(failures) + + success = len(docnames) - len(failures) + frappe.msgprint( + _('{} e-invoices generated successfully').format(success), + title=_('Bulk E-Invoice Generation Complete') + ) + + else: + enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames) + +def schedule_bulk_generate_irn(docnames): + failures = GSPConnector.bulk_generate_irn(docnames) + frappe.local.message_log = [] + + frappe.publish_realtime("bulk_einvoice_generation_complete", { + "user": frappe.session.user, + "failures": failures, + "invoices": docnames + }) + +def show_bulk_action_failure_message(failures): + for doc in failures: + docname = '{0}'.format(doc.get('docname')) + message = doc.get('message').replace("'", '"') + if message[0] == '[': + errors = json.loads(message) + error_list = ''.join(['
  • {}
  • '.format(err) for err in errors]) + message = '''{} has following errors:
    + '''.format(docname, error_list) + else: + message = '{} - {}'.format(docname, message) + + frappe.msgprint( + message, + title=_('Bulk E-Invoice Generation Complete'), + indicator='red' + ) + +@frappe.whitelist() +def cancel_irns(docnames, reason, remark): + docnames = json.loads(docnames) or [] + + if len(docnames) < 10: + failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark) + frappe.local.message_log = [] + + if failures: + show_bulk_action_failure_message(failures) + + success = len(docnames) - len(failures) + frappe.msgprint( + _('{} e-invoices cancelled successfully').format(success), + title=_('Bulk E-Invoice Cancellation Complete') + ) + else: + enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark) + +def schedule_bulk_cancel_irn(docnames, reason, remark): + failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark) + frappe.local.message_log = [] + + frappe.publish_realtime("bulk_einvoice_cancellation_complete", { + "user": frappe.session.user, + "failures": failures, + "invoices": docnames + }) + +def enqueue_bulk_action(job, **kwargs): + check_scheduler_status() + + enqueue( + job, + **kwargs, + queue="long", + timeout=10000, + event="processing_bulk_einvoice_action", + now=frappe.conf.developer_mode or frappe.flags.in_test, + ) + + if job == schedule_bulk_generate_irn: + msg = _('E-Invoices will be generated in a background process.') + else: + msg = _('E-Invoices will be cancelled in a background process.') + + frappe.msgprint(msg, alert=1) + +def check_scheduler_status(): + if is_scheduler_inactive() and not frappe.flags.in_test: + frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) + +def job_already_enqueued(job_name): + enqueued_jobs = [d.get("job_name") for d in get_info()] + if job_name in enqueued_jobs: + return True diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index c0dcb70b92c..074bd527e2c 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -60,7 +60,7 @@ def create_hsn_codes(data, code_field): def add_custom_roles_for_reports(): for report_name in ('GST Sales Register', 'GST Purchase Register', - 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill'): + 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'): if not frappe.db.get_value('Custom Role', dict(report=report_name)): frappe.get_doc(dict( @@ -99,7 +99,7 @@ def add_custom_roles_for_reports(): )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): + for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): add_permission(doctype, role, 0) @@ -115,9 +115,11 @@ def add_permissions(): def add_print_formats(): frappe.reload_doc("regional", "print_format", "gst_tax_invoice") frappe.reload_doc("accounts", "print_format", "gst_pos_invoice") + frappe.reload_doc("accounts", "print_format", "GST E-Invoice") frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0) frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0) + frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters @@ -453,7 +455,7 @@ def get_custom_fields(): 'fieldname': 'ewaybill', 'label': 'E-Way Bill No.', 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', + 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)', 'allow_on_submit': 1, 'insert_after': 'tax_id', 'translatable': 0, @@ -481,6 +483,46 @@ def get_custom_fields(): fetch_from='customer_address.gstin', print_hide=1, read_only=1) ] + si_einvoice_fields = [ + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval: doc.irn', allow_on_submit=1, insert_after='customer'), + + dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1, + depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill'), + + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', + print_hide=1, hidden=1), + + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', + no_copy=1, print_hide=1), + + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), + + dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', + no_copy=1, print_hide=1), + + dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', + no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice', + no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code', + no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image', + options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON', + hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1) + ] + custom_fields = { 'Address': [ dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', @@ -493,7 +535,7 @@ def get_custom_fields(): 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, 'Purchase Order': purchase_invoice_gst_fields, 'Purchase Receipt': purchase_invoice_gst_fields, - 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields, + 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, 'POS Invoice': sales_invoice_gst_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category, 'Payment Entry': payment_entry_fields, @@ -567,16 +609,16 @@ def get_custom_fields(): fieldtype='Link', options='Salary Component', insert_after='basic_component'), dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'), dict(fieldname='arrear_component', label='Arrear Component', - fieldtype='Link', options='Salary Component', insert_after='hra_component'), + fieldtype='Link', options='Salary Component', insert_after='hra_column_break'), dict(fieldname='non_profit_section', label='Non Profit Settings', - fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1), + fieldtype='Section Break', insert_after='arrear_component', collapsible=1), dict(fieldname='company_80g_number', label='80G Number', fieldtype='Data', insert_after='non_profit_section'), dict(fieldname='with_effect_from', label='80G With Effect From', fieldtype='Date', insert_after='company_80g_number'), dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'), dict(fieldname='pan_details', label='PAN Number', - fieldtype='Data', insert_after='with_effect_from') + fieldtype='Data', insert_after='non_profit_column_break') ], 'Employee Tax Exemption Declaration':[ dict(fieldname='hra_section', label='HRA Exemption', diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index d443f9c15c0..8715ef57bab 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -219,6 +219,7 @@ def get_regional_address_details(party_details, doctype, company): if not party_details.place_of_supply: return party_details if not party_details.company_gstin: return party_details + if not party_details.supplier_gstin: return party_details if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index 14d5495eed7..052fb2a7244 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -1,9 +1,9 @@ import zipfile +from io import BytesIO from unittest import TestCase import frappe from frappe.utils import cstr, now_datetime, today -from six import BytesIO from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.regional.germany.utils.datev.datev_constants import ( diff --git a/erpnext/hotels/doctype/hotel_room/__init__.py b/erpnext/regional/report/e_invoice_summary/__init__.py similarity index 100% rename from erpnext/hotels/doctype/hotel_room/__init__.py rename to erpnext/regional/report/e_invoice_summary/__init__.py diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js new file mode 100644 index 00000000000..4713217d83c --- /dev/null +++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js @@ -0,0 +1,55 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["E-Invoice Summary"] = { + "filters": [ + { + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "fieldname": "company", + "label": __("Company"), + "default": frappe.defaults.get_user_default("Company"), + }, + { + "fieldtype": "Link", + "options": "Customer", + "fieldname": "customer", + "label": __("Customer") + }, + { + "fieldtype": "Date", + "reqd": 1, + "fieldname": "from_date", + "label": __("From Date"), + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldtype": "Date", + "reqd": 1, + "fieldname": "to_date", + "label": __("To Date"), + "default": frappe.datetime.get_today(), + }, + { + "fieldtype": "Select", + "fieldname": "status", + "label": __("Status"), + "options": "\nPending\nGenerated\nCancelled\nFailed" + } + ], + + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "einvoice_status" && value) { + if (value == 'Pending') value = `${value}`; + else if (value == 'Generated') value = `${value}`; + else if (value == 'Cancelled') value = `${value}`; + else if (value == 'Failed') value = `${value}`; + } + + return value; + } +}; diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json new file mode 100644 index 00000000000..d0000ad50df --- /dev/null +++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json @@ -0,0 +1,28 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-03-12 11:23:37.312294", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letter_head": "Logo", + "modified": "2021-03-13 12:36:48.689413", + "modified_by": "Administrator", + "module": "Regional", + "name": "E-Invoice Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Invoice", + "report_name": "E-Invoice Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Administrator" + } + ] +} \ No newline at end of file diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py new file mode 100644 index 00000000000..2110c444470 --- /dev/null +++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py @@ -0,0 +1,110 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ + + +def execute(filters=None): + validate_filters(filters) + + columns = get_columns() + data = get_data(filters) + + return columns, data + +def validate_filters(filters=None): + if filters is None: + filters = {} + filters = frappe._dict(filters) + + if not filters.company: + frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter')) + if filters.company: + # validate if company has e-invoicing enabled + pass + if not filters.from_date or not filters.to_date: + frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter')) + if filters.from_date > filters.to_date: + frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter')) + +def get_data(filters=None): + if filters is None: + filters = {} + query_filters = { + 'posting_date': ['between', [filters.from_date, filters.to_date]], + 'einvoice_status': ['is', 'set'], + 'company': filters.company + } + if filters.customer: + query_filters['customer'] = filters.customer + if filters.status: + query_filters['einvoice_status'] = filters.status + + data = frappe.get_all( + 'Sales Invoice', + filters=query_filters, + fields=[d.get('fieldname') for d in get_columns()] + ) + + return data + +def get_columns(): + return [ + { + "fieldtype": "Date", + "fieldname": "posting_date", + "label": _("Posting Date"), + "width": 0 + }, + { + "fieldtype": "Link", + "fieldname": "name", + "label": _("Sales Invoice"), + "options": "Sales Invoice", + "width": 140 + }, + { + "fieldtype": "Data", + "fieldname": "einvoice_status", + "label": _("Status"), + "width": 100 + }, + { + "fieldtype": "Link", + "fieldname": "customer", + "options": "Customer", + "label": _("Customer") + }, + { + "fieldtype": "Check", + "fieldname": "is_return", + "label": _("Is Return"), + "width": 85 + }, + { + "fieldtype": "Data", + "fieldname": "ack_no", + "label": "Ack. No.", + "width": 145 + }, + { + "fieldtype": "Data", + "fieldname": "ack_date", + "label": "Ack. Date", + "width": 165 + }, + { + "fieldtype": "Data", + "fieldname": "irn", + "label": _("IRN No."), + "width": 250 + }, + { + "fieldtype": "Currency", + "options": "Company:company:default_currency", + "fieldname": "base_grand_total", + "label": _("Grand Total"), + "width": 120 + } + ] diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index ef2bdb67980..4b98978f130 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -53,7 +53,8 @@ frappe.query_reports["GSTR-1"] = { { "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") }, { "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") }, { "value": "EXPORT", "label": __("Export Invoice - 6A") }, - { "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") } + { "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") }, + { "value": "NIL Rated", "label": __("NIL RATED/EXEMPTED Invoices") } ], "default": "B2B" } diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 11b684d3f6a..e50ff180328 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -40,7 +40,8 @@ class Gstr1Report(object): port_code, shipping_bill_number, shipping_bill_date, - reason_for_issuing_document + reason_for_issuing_document, + company_gstin """ def run(self): @@ -62,6 +63,8 @@ class Gstr1Report(object): self.get_b2c_data() elif self.filters.get("type_of_business") == "Advances": self.get_advance_data() + elif self.filters.get("type_of_business") == "NIL Rated": + self.get_nil_rated_invoices() elif self.invoices: for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) @@ -91,6 +94,57 @@ class Gstr1Report(object): row= [key[0], key[1], value[0], value[1]] self.data.append(row) + def get_nil_rated_invoices(self): + nil_exempt_output = [ + { + "description": "Inter-State supplies to registered persons", + "nil_rated": 0.0, + "exempted": 0.0, + "non_gst": 0.0 + }, + { + "description": "Intra-State supplies to registered persons", + "nil_rated": 0.0, + "exempted": 0.0, + "non_gst": 0.0 + }, + { + "description": "Inter-State supplies to unregistered persons", + "nil_rated": 0.0, + "exempted": 0.0, + "non_gst": 0.0 + }, + { + "description": "Intra-State supplies to unregistered persons", + "nil_rated": 0.0, + "exempted": 0.0, + "non_gst": 0.0 + } + ] + + for invoice, details in self.nil_exempt_non_gst.items(): + invoice_detail = self.invoices.get(invoice) + if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"): + if is_inter_state(invoice_detail): + nil_exempt_output[0]["nil_rated"] += details[0] + nil_exempt_output[0]["exempted"] += details[1] + nil_exempt_output[0]["non_gst"] += details[2] + else: + nil_exempt_output[1]["nil_rated"] += details[0] + nil_exempt_output[1]["exempted"] += details[1] + nil_exempt_output[1]["non_gst"] += details[2] + else: + if is_inter_state(invoice_detail): + nil_exempt_output[2]["nil_rated"] += details[0] + nil_exempt_output[2]["exempted"] += details[1] + nil_exempt_output[2]["non_gst"] += details[2] + else: + nil_exempt_output[3]["nil_rated"] += details[0] + nil_exempt_output[3]["exempted"] += details[1] + nil_exempt_output[3]["non_gst"] += details[2] + + self.data = nil_exempt_output + def get_b2c_data(self): b2cs_output = {} @@ -240,10 +294,11 @@ class Gstr1Report(object): def get_invoice_items(self): self.invoice_items = frappe._dict() self.item_tax_rate = frappe._dict() + self.nil_exempt_non_gst = {} items = frappe.db.sql(""" - select item_code, parent, taxable_value, base_net_amount, item_tax_rate - from `tab%s Item` + select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt, + is_non_gst from `tab%s Item` where parent in (%s) """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) @@ -260,6 +315,16 @@ class Gstr1Report(object): tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, []) tax_rate_dict.append(rate) + if d.is_nil_exempt: + self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) + if item_tax_rate: + self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0) + else: + self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0) + elif d.is_non_gst: + self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) + self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0) + def get_items_based_on_tax_rate(self): self.tax_details = frappe.db.sql(""" select @@ -322,21 +387,24 @@ class Gstr1Report(object): self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): - self.tax_columns = [ - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Int", - "width": 60 - }, - { - "fieldname": "taxable_value", - "label": "Taxable Value", - "fieldtype": "Currency", - "width": 100 - } - ] self.other_columns = [] + self.tax_columns = [] + + if self.filters.get("type_of_business") != "NIL Rated": + self.tax_columns = [ + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Int", + "width": 60 + }, + { + "fieldname": "taxable_value", + "label": "Taxable Value", + "fieldtype": "Currency", + "width": 100 + } + ] if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ @@ -705,6 +773,33 @@ class Gstr1Report(object): "width": 100 } ] + elif self.filters.get("type_of_business") == "NIL Rated": + self.invoice_columns = [ + { + "fieldname": "description", + "label": "Description", + "fieldtype": "Data", + "width": 420 + }, + { + "fieldname": "nil_rated", + "label": "Nil Rated", + "fieldtype": "Currency", + "width": 200 + }, + { + "fieldname": "exempted", + "label": "Exempted", + "fieldtype": "Currency", + "width": 200 + }, + { + "fieldname": "non_gst", + "label": "Non GST", + "fieldtype": "Currency", + "width": 200 + } + ] self.columns = self.invoice_columns + self.tax_columns + self.other_columns @@ -768,6 +863,11 @@ def get_json(filters, report_name, data): out = get_advances_json(res, gstin) gst_json["at"] = out + elif filters["type_of_business"] == "NIL Rated": + res = report_data[:-1] + out = get_exempted_json(res) + gst_json["nil"] = out + return { 'report_name': report_name, 'report_type': filters['type_of_business'], @@ -980,6 +1080,36 @@ def get_cdnr_unreg_json(res, gstin): return out +def get_exempted_json(data): + out = { + "inv": [ + { + "sply_ty": "INTRB2B" + }, + { + "sply_ty": "INTRAB2B" + }, + { + "sply_ty": "INTRB2C" + }, + { + "sply_ty": "INTRAB2C" + } + ] + } + + for i, v in enumerate(data): + if data[i].get('nil_rated'): + out['inv'][i]['nil_amt'] = data[i]['nil_rated'] + + if data[i].get('exempted'): + out['inv'][i]['expt_amt'] = data[i]['exempted'] + + if data[i].get('non_gst'): + out['inv'][i]['ngsup_amt'] = data[i]['non_gst'] + + return out + def get_invoice_type_for_cdnr(row): if row.get('gst_category') == 'SEZ': if row.get('export_type') == 'WPAY': @@ -1064,3 +1194,9 @@ def download_json_file(): frappe.response['filecontent'] = data['data'] frappe.response['content_type'] = 'application/json' frappe.response['type'] = 'download' + +def is_inter_state(invoice_detail): + if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]: + return True + else: + return False \ No newline at end of file diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 2e31c03d5c6..15d524d5b81 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -3,12 +3,10 @@ import frappe from frappe.permissions import add_permission, update_permission_property -from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def setup(company=None, patch=True): - uae_custom_fields() add_print_formats() add_permissions() make_custom_fields() @@ -40,38 +38,67 @@ def make_custom_fields(): - Company Name in Arabic - Address in Arabic """ + is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', + fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description', + print_hide=1) + + is_exempt = dict(fieldname='is_exempt', label='Is Exempt', + fieldtype='Check', fetch_from='item_code.is_exempt', insert_after='is_zero_rated', + print_hide=1) + + purchase_invoice_fields = [ + dict(fieldname='company_trn', label='Company TRN', + fieldtype='Read Only', insert_after='shipping_address', + fetch_from='company.tax_id', print_hide=1), + dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', + fieldtype='Read Only', insert_after='supplier_name', + fetch_from='supplier.supplier_name_in_arabic', print_hide=1) + ] + + sales_invoice_fields = [ + dict(fieldname='company_trn', label='Company TRN', + fieldtype='Read Only', insert_after='company_address', + fetch_from='company.tax_id', print_hide=1), + dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', + fieldtype='Read Only', insert_after='customer_name', + fetch_from='customer.customer_name_in_arabic', print_hide=1), + dict(fieldname='ksa_einv_qr', label='KSA E-Invoicing QR', + fieldtype='Attach Image', read_only=1, no_copy=1, hidden=1) + ] + custom_fields = { - 'Sales Invoice': [ - dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 - ) + 'Item': [is_zero_rated, is_exempt], + 'Customer': [ + dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', + fieldtype='Data', insert_after='customer_name'), ], - 'POS Invoice': [ - dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 - ) + 'Supplier': [ + dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', + fieldtype='Data', insert_after='supplier_name'), ], + 'Purchase Invoice': purchase_invoice_fields, + 'Purchase Order': purchase_invoice_fields, + 'Purchase Receipt': purchase_invoice_fields, + 'Sales Invoice': sales_invoice_fields, + 'POS Invoice': sales_invoice_fields, + 'Sales Order': sales_invoice_fields, + 'Delivery Note': sales_invoice_fields, + 'Sales Invoice Item': [is_zero_rated, is_exempt], + 'POS Invoice Item': [is_zero_rated, is_exempt], + 'Purchase Invoice Item': [is_zero_rated, is_exempt], + 'Sales Order Item': [is_zero_rated, is_exempt], + 'Delivery Note Item': [is_zero_rated, is_exempt], + 'Quotation Item': [is_zero_rated, is_exempt], + 'Purchase Order Item': [is_zero_rated, is_exempt], + 'Purchase Receipt Item': [is_zero_rated, is_exempt], + 'Supplier Quotation Item': [is_zero_rated, is_exempt], 'Address': [ - dict( - fieldname='address_in_arabic', - label='Address in Arabic', - fieldtype='Data', - insert_after='address_line2' - ) + dict(fieldname='address_in_arabic', label='Address in Arabic', + fieldtype='Data',insert_after='address_line2') ], 'Company': [ - dict( - fieldname='company_name_in_arabic', - label='Company Name In Arabic', - fieldtype='Data', - insert_after='company_name' - ) + dict(fieldname='company_name_in_arabic', label='Company Name In Arabic', + fieldtype='Data', insert_after='company_name') ] } diff --git a/erpnext/restaurant/__init__.py b/erpnext/restaurant/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/restaurant/doctype/__init__.py b/erpnext/restaurant/doctype/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/restaurant/doctype/restaurant/__init__.py b/erpnext/restaurant/doctype/restaurant/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/restaurant/doctype/restaurant/restaurant.js b/erpnext/restaurant/doctype/restaurant/restaurant.js deleted file mode 100644 index 13fda73922a..00000000000 --- a/erpnext/restaurant/doctype/restaurant/restaurant.js +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Restaurant', { - refresh: function(frm) { - frm.add_custom_button(__('Order Entry'), () => { - frappe.set_route('Form', 'Restaurant Order Entry'); - }); - } -}); diff --git a/erpnext/restaurant/doctype/restaurant/restaurant.json b/erpnext/restaurant/doctype/restaurant/restaurant.json deleted file mode 100644 index 85726874119..00000000000 --- a/erpnext/restaurant/doctype/restaurant/restaurant.json +++ /dev/null @@ -1,309 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "prompt", - "beta": 1, - "creation": "2017-09-15 12:40:41.546933", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 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": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_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": "Default 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "invoice_series_prefix", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Invoice Series Prefix", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "active_menu", - "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": "Active Menu", - "length": 0, - "no_copy": 0, - "options": "Restaurant Menu", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_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": "Default 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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address", - "length": 0, - "no_copy": 0, - "options": "Address", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-09 12:13:10.185496", - "modified_by": "Administrator", - "module": "Restaurant", - "name": "Restaurant", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/restaurant/doctype/restaurant/restaurant.py b/erpnext/restaurant/doctype/restaurant/restaurant.py deleted file mode 100644 index 67838d29a37..00000000000 --- a/erpnext/restaurant/doctype/restaurant/restaurant.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class Restaurant(Document): - pass diff --git a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py b/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py deleted file mode 100644 index bfdd052753e..00000000000 --- a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py +++ /dev/null @@ -1,17 +0,0 @@ -from frappe import _ - - -def get_data(): - return { - 'fieldname': 'restaurant', - 'transactions': [ - { - 'label': _('Setup'), - 'items': ['Restaurant Menu', 'Restaurant Table'] - }, - { - 'label': _('Operations'), - 'items': ['Restaurant Reservation', 'Sales Invoice'] - } - ] - } diff --git a/erpnext/restaurant/doctype/restaurant/test_restaurant.py b/erpnext/restaurant/doctype/restaurant/test_restaurant.py deleted file mode 100644 index f88f9801290..00000000000 --- a/erpnext/restaurant/doctype/restaurant/test_restaurant.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -test_records = [ - dict(doctype='Restaurant', name='Test Restaurant 1', company='_Test Company 1', - invoice_series_prefix='Test-Rest-1-Inv-', default_customer='_Test Customer 1'), - dict(doctype='Restaurant', name='Test Restaurant 2', company='_Test Company 1', - invoice_series_prefix='Test-Rest-2-Inv-', default_customer='_Test Customer 1'), -] - -class TestRestaurant(unittest.TestCase): - pass diff --git a/erpnext/restaurant/doctype/restaurant_menu/__init__.py b/erpnext/restaurant/doctype/restaurant_menu/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.js b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.js deleted file mode 100644 index da7d43f8a3e..00000000000 --- a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Restaurant Menu', { - setup: function(frm) { - frm.add_fetch('item', 'standard_rate', 'rate'); - }, -}); diff --git a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.json b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.json deleted file mode 100644 index 1b1610dbacf..00000000000 --- a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.json +++ /dev/null @@ -1,247 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "prompt", - "beta": 1, - "creation": "2017-09-15 12:48:29.818715", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "restaurant", - "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": "Restaurant", - "length": 0, - "no_copy": 0, - "options": "Restaurant", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fieldname": "enabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Enabled", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "price_list", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Price List (Auto created)", - "length": 0, - "no_copy": 0, - "options": "Price List", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "items_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Items", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Items", - "length": 0, - "no_copy": 0, - "options": "Restaurant Menu 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-09 12:13:13.684500", - "modified_by": "Administrator", - "module": "Restaurant", - "name": "Restaurant Menu", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Restaurant Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py deleted file mode 100644 index 64eb40f3645..00000000000 --- a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe.model.document import Document - - -class RestaurantMenu(Document): - def validate(self): - for d in self.items: - if not d.rate: - d.rate = frappe.db.get_value('Item', d.item, 'standard_rate') - - def on_update(self): - '''Sync Price List''' - self.make_price_list() - - def on_trash(self): - '''clear prices''' - self.clear_item_price() - - def clear_item_price(self, price_list=None): - '''clear all item prices for this menu''' - if not price_list: - price_list = self.get_price_list().name - frappe.db.sql('delete from `tabItem Price` where price_list = %s', price_list) - - def make_price_list(self): - # create price list for menu - price_list = self.get_price_list() - self.db_set('price_list', price_list.name) - - # delete old items - self.clear_item_price(price_list.name) - - for d in self.items: - frappe.get_doc(dict( - doctype = 'Item Price', - price_list = price_list.name, - item_code = d.item, - price_list_rate = d.rate - )).insert() - - def get_price_list(self): - '''Create price list for menu if missing''' - price_list_name = frappe.db.get_value('Price List', dict(restaurant_menu=self.name)) - if price_list_name: - price_list = frappe.get_doc('Price List', price_list_name) - else: - price_list = frappe.new_doc('Price List') - price_list.restaurant_menu = self.name - price_list.price_list_name = self.name - - price_list.enabled = 1 - price_list.selling = 1 - price_list.save() - - return price_list diff --git a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py b/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py deleted file mode 100644 index 27020eb869f..00000000000 --- a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -import frappe - -test_records = [ - dict(doctype='Item', item_code='Food Item 1', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Food Item 2', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Food Item 3', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Food Item 4', - item_group='Products', is_stock_item=0), - dict(doctype='Restaurant Menu', restaurant='Test Restaurant 1', name='Test Restaurant 1 Menu 1', - items = [ - dict(item='Food Item 1', rate=400), - dict(item='Food Item 2', rate=300), - dict(item='Food Item 3', rate=200), - dict(item='Food Item 4', rate=100), - ]), - dict(doctype='Restaurant Menu', restaurant='Test Restaurant 1', name='Test Restaurant 1 Menu 2', - items = [ - dict(item='Food Item 1', rate=450), - dict(item='Food Item 2', rate=350), - ]) -] - -class TestRestaurantMenu(unittest.TestCase): - def test_price_list_creation_and_editing(self): - menu1 = frappe.get_doc('Restaurant Menu', 'Test Restaurant 1 Menu 1') - menu1.save() - - menu2 = frappe.get_doc('Restaurant Menu', 'Test Restaurant 1 Menu 2') - menu2.save() - - self.assertTrue(frappe.db.get_value('Price List', 'Test Restaurant 1 Menu 1')) - self.assertEqual(frappe.db.get_value('Item Price', - dict(price_list = 'Test Restaurant 1 Menu 1', item_code='Food Item 1'), 'price_list_rate'), 400) - self.assertEqual(frappe.db.get_value('Item Price', - dict(price_list = 'Test Restaurant 1 Menu 2', item_code='Food Item 1'), 'price_list_rate'), 450) - - menu1.items[0].rate = 401 - menu1.save() - - self.assertEqual(frappe.db.get_value('Item Price', - dict(price_list = 'Test Restaurant 1 Menu 1', item_code='Food Item 1'), 'price_list_rate'), 401) - - menu1.items[0].rate = 400 - menu1.save() diff --git a/erpnext/restaurant/doctype/restaurant_menu_item/__init__.py b/erpnext/restaurant/doctype/restaurant_menu_item/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.json b/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.json deleted file mode 100644 index 87568bf9818..00000000000 --- a/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-09-15 12:49:36.072636", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 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": 1, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Rate", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-09-15 14:18:55.145088", - "modified_by": "Administrator", - "module": "Restaurant", - "name": "Restaurant Menu Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.py b/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.py deleted file mode 100644 index 98b245edece..00000000000 --- a/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class RestaurantMenuItem(Document): - pass diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/__init__.py b/erpnext/restaurant/doctype/restaurant_order_entry/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.js b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.js deleted file mode 100644 index 8df851c62b1..00000000000 --- a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.js +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Restaurant Order Entry', { - setup: function(frm) { - let get_item_query = () => { - return { - query: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.item_query_restaurant', - filters: { - 'table': frm.doc.restaurant_table - } - }; - }; - frm.set_query('item', 'items', get_item_query); - frm.set_query('add_item', get_item_query); - }, - onload_post_render: function(frm) { - if(!frm.item_selector) { - frm.item_selector = new erpnext.ItemSelector({ - frm: frm, - item_field: 'item', - item_query: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.item_query_restaurant', - get_filters: () => { - return {table: frm.doc.restaurant_table}; - } - }); - } - - let $input = frm.get_field('add_item').$input; - - $input.on('keyup', function(e) { - if (e.which===13) { - if (frm.clear_item_timeout) { - clearTimeout (frm.clear_item_timeout); - } - - // clear the item input so user can enter a new item - frm.clear_item_timeout = setTimeout (() => { - frm.set_value('add_item', ''); - }, 1000); - - let item = $input.val(); - - if (!item) return; - - var added = false; - (frm.doc.items || []).forEach((d) => { - if (d.item===item) { - d.qty += 1; - added = true; - } - }); - - return frappe.run_serially([ - () => { - if (!added) { - return frm.add_child('items', {item: item, qty: 1}); - } - }, - () => frm.get_field("items").refresh() - ]); - } - }); - }, - refresh: function(frm) { - frm.disable_save(); - frm.add_custom_button(__('Update'), () => { - return frm.trigger('sync'); - }); - frm.add_custom_button(__('Clear'), () => { - return frm.trigger('clear'); - }); - frm.add_custom_button(__('Bill'), () => { - return frm.trigger('make_invoice'); - }); - }, - clear: function(frm) { - frm.doc.add_item = ''; - frm.doc.grand_total = 0; - frm.doc.items = []; - frm.refresh(); - frm.get_field('add_item').$input.focus(); - }, - restaurant_table: function(frm) { - // select the open sales order items for this table - if (!frm.doc.restaurant_table) { - return; - } - return frappe.call({ - method: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.get_invoice', - args: { - table: frm.doc.restaurant_table - }, - callback: (r) => { - frm.events.set_invoice_items(frm, r); - } - }); - }, - sync: function(frm) { - return frappe.call({ - method: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.sync', - args: { - table: frm.doc.restaurant_table, - items: frm.doc.items - }, - callback: (r) => { - frm.events.set_invoice_items(frm, r); - frappe.show_alert({message: __('Saved'), indicator: 'green'}); - } - }); - - }, - make_invoice: function(frm) { - frm.trigger('sync').then(() => { - frappe.prompt([ - { - fieldname: 'customer', - label: __('Customer'), - fieldtype: 'Link', - reqd: 1, - options: 'Customer', - 'default': frm.invoice.customer - }, - { - fieldname: 'mode_of_payment', - label: __('Mode of Payment'), - fieldtype: 'Link', - reqd: 1, - options: 'Mode of Payment', - 'default': frm.mode_of_payment || '' - } - ], (data) => { - // cache this for next entry - frm.mode_of_payment = data.mode_of_payment; - return frappe.call({ - method: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.make_invoice', - args: { - table: frm.doc.restaurant_table, - customer: data.customer, - mode_of_payment: data.mode_of_payment - }, - callback: (r) => { - frm.set_value('last_sales_invoice', r.message); - frm.trigger('clear'); - } - }); - }, - __("Select Customer")); - }); - }, - set_invoice_items: function(frm, r) { - let invoice = r.message; - frm.doc.items = []; - (invoice.items || []).forEach((d) => { - frm.add_child('items', {item: d.item_code, qty: d.qty, rate: d.rate}); - }); - frm.set_value('grand_total', invoice.grand_total); - frm.set_value('last_sales_invoice', invoice.name); - frm.invoice = invoice; - frm.refresh(); - } -}); diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.json b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.json deleted file mode 100644 index 3e4d593d5b6..00000000000 --- a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.json +++ /dev/null @@ -1,280 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 1, - "creation": "2017-09-15 15:10:24.530365", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "restaurant_table", - "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": "Restaurant Table", - "length": 0, - "no_copy": 0, - "options": "Restaurant Table", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "restaurant_table", - "description": "Click Enter To Add", - "fieldname": "add_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": "Add 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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "grand_total", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Grand Total", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "last_sales_invoice", - "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": "Last Sales Invoice", - "length": 0, - "no_copy": 0, - "options": "Sales Invoice", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "restaurant_table", - "fieldname": "current_order", - "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": "Current Order", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "restaurant_table", - "fieldname": "items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Items", - "length": 0, - "no_copy": 0, - "options": "Restaurant Order Entry 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, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2017-10-04 17:06:20.926999", - "modified_by": "Administrator", - "module": "Restaurant", - "name": "Restaurant Order Entry", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Restaurant Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py deleted file mode 100644 index f9e75b47a0f..00000000000 --- a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import json - -import frappe -from frappe import _ -from frappe.model.document import Document - -from erpnext.controllers.queries import item_query - - -class RestaurantOrderEntry(Document): - pass - -@frappe.whitelist() -def get_invoice(table): - '''returns the active invoice linked to the given table''' - invoice_name = frappe.get_value('Sales Invoice', dict(restaurant_table = table, docstatus=0)) - restaurant, menu_name = get_restaurant_and_menu_name(table) - if invoice_name: - invoice = frappe.get_doc('Sales Invoice', invoice_name) - else: - invoice = frappe.new_doc('Sales Invoice') - invoice.naming_series = frappe.db.get_value('Restaurant', restaurant, 'invoice_series_prefix') - invoice.is_pos = 1 - default_customer = frappe.db.get_value('Restaurant', restaurant, 'default_customer') - if not default_customer: - frappe.throw(_('Please set default customer in Restaurant Settings')) - invoice.customer = default_customer - - invoice.taxes_and_charges = frappe.db.get_value('Restaurant', restaurant, 'default_tax_template') - invoice.selling_price_list = frappe.db.get_value('Price List', dict(restaurant_menu=menu_name, enabled=1)) - - return invoice - -@frappe.whitelist() -def sync(table, items): - '''Sync the sales order related to the table''' - invoice = get_invoice(table) - items = json.loads(items) - - invoice.items = [] - invoice.restaurant_table = table - for d in items: - invoice.append('items', dict( - item_code = d.get('item'), - qty = d.get('qty') - )) - - invoice.save() - return invoice.as_dict() - -@frappe.whitelist() -def make_invoice(table, customer, mode_of_payment): - '''Make table based on Sales Order''' - restaurant, menu = get_restaurant_and_menu_name(table) - invoice = get_invoice(table) - invoice.customer = customer - invoice.restaurant = restaurant - invoice.calculate_taxes_and_totals() - invoice.append('payments', dict(mode_of_payment=mode_of_payment, amount=invoice.grand_total)) - invoice.save() - invoice.submit() - - frappe.msgprint(_('Invoice Created'), indicator='green', alert=True) - - return invoice.name - -@frappe.whitelist() -def item_query_restaurant(doctype='Item', txt='', searchfield='name', start=0, page_len=20, filters=None, as_dict=False): - '''Return items that are selected in active menu of the restaurant''' - restaurant, menu = get_restaurant_and_menu_name(filters['table']) - items = frappe.db.get_all('Restaurant Menu Item', ['item'], dict(parent = menu)) - del filters['table'] - filters['name'] = ('in', [d.item for d in items]) - - return item_query('Item', txt, searchfield, start, page_len, filters, as_dict) - -def get_restaurant_and_menu_name(table): - if not table: - frappe.throw(_('Please select a table')) - - restaurant = frappe.db.get_value('Restaurant Table', table, 'restaurant') - menu = frappe.db.get_value('Restaurant', restaurant, 'active_menu') - - if not menu: - frappe.throw(_('Please set an active menu for Restaurant {0}').format(restaurant)) - - return restaurant, menu diff --git a/erpnext/restaurant/doctype/restaurant_order_entry_item/__init__.py b/erpnext/restaurant/doctype/restaurant_order_entry_item/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.json b/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.json deleted file mode 100644 index 0240013c784..00000000000 --- a/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-09-15 15:11:50.313241", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 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": 1, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "qty", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Qty", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "served", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Served", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Rate", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-09-21 08:39:27.232175", - "modified_by": "Administrator", - "module": "Restaurant", - "name": "Restaurant Order Entry Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.py b/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.py deleted file mode 100644 index 0d9c236c0ea..00000000000 --- a/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class RestaurantOrderEntryItem(Document): - pass diff --git a/erpnext/restaurant/doctype/restaurant_reservation/__init__.py b/erpnext/restaurant/doctype/restaurant_reservation/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.js b/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.js deleted file mode 100644 index cebd1052a81..00000000000 --- a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.js +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Restaurant Reservation', { - setup: function(frm) { - frm.add_fetch('customer', 'customer_name', 'customer_name'); - }, - refresh: function(frm) { - - } -}); diff --git a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.json b/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.json deleted file mode 100644 index 17df2b931de..00000000000 --- a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.json +++ /dev/null @@ -1,355 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "RES-RES-.YYYY.-.#####", - "beta": 1, - "creation": "2017-09-15 13:05:51.063661", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "status", - "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": 0, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Open\nWaitlisted\nCancelled\nNo Show\nSuccess", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "restaurant", - "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": "Restaurant", - "length": 0, - "no_copy": 0, - "options": "Restaurant", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "no_of_people", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "No of People", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reservation_time", - "fieldtype": "Datetime", - "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": "Reservation Time", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reservation_end_time", - "fieldtype": "Datetime", - "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": "Reservation End Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Customer Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_number", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Contact Number", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:38.435656", - "modified_by": "Administrator", - "module": "Restaurant", - "name": "Restaurant Reservation", - "name_case": "", - "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": "Restaurant Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.py b/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.py deleted file mode 100644 index 02ffaf6c20b..00000000000 --- a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from datetime import timedelta - -from frappe.model.document import Document -from frappe.utils import get_datetime - - -class RestaurantReservation(Document): - def validate(self): - if not self.reservation_end_time: - self.reservation_end_time = get_datetime(self.reservation_time) + timedelta(hours=1) diff --git a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation_calendar.js b/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation_calendar.js deleted file mode 100644 index fe3dc57a720..00000000000 --- a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation_calendar.js +++ /dev/null @@ -1,18 +0,0 @@ -frappe.views.calendar["Restaurant Reservation"] = { - field_map: { - "start": "reservation_time", - "end": "reservation_end_time", - "id": "name", - "title": "customer_name", - "allDay": "allDay", - }, - gantt: true, - filters: [ - { - "fieldtype": "Data", - "fieldname": "customer_name", - "label": __("Customer Name") - } - ], - get_events_method: "frappe.desk.calendar.get_events" -}; diff --git a/erpnext/restaurant/doctype/restaurant_reservation/test_restaurant_reservation.py b/erpnext/restaurant/doctype/restaurant_reservation/test_restaurant_reservation.py deleted file mode 100644 index 11a3541bd56..00000000000 --- a/erpnext/restaurant/doctype/restaurant_reservation/test_restaurant_reservation.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestRestaurantReservation(unittest.TestCase): - pass diff --git a/erpnext/restaurant/doctype/restaurant_table/__init__.py b/erpnext/restaurant/doctype/restaurant_table/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.js b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.js deleted file mode 100644 index a55605c90bf..00000000000 --- a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Restaurant Table', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.json b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.json deleted file mode 100644 index 5fc6e62ecb9..00000000000 --- a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 1, - "creation": "2017-09-15 12:45:24.717355", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "restaurant", - "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": "Restaurant", - "length": 0, - "no_copy": 0, - "options": "Restaurant", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "no_of_seats", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "No of Seats", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fieldname": "minimum_seating", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Minimum Seating", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-09 12:13:24.382345", - "modified_by": "Administrator", - "module": "Restaurant", - "name": "Restaurant Table", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Restaurant Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Hospitality", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py deleted file mode 100644 index 29f8a1a12b1..00000000000 --- a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import re - -from frappe.model.document import Document -from frappe.model.naming import make_autoname - - -class RestaurantTable(Document): - def autoname(self): - prefix = re.sub('-+', '-', self.restaurant.replace(' ', '-')) - self.name = make_autoname(prefix + '-.##') diff --git a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py b/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py deleted file mode 100644 index 00d14d2bb2a..00000000000 --- a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -test_records = [ - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), -] - -class TestRestaurantTable(unittest.TestCase): - pass diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 40c8a37f4e7..7742f26ad11 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -142,7 +142,7 @@ class Customer(TransactionBase): self.update_lead_status() if self.flags.is_new_doc: - self.create_lead_address_contact() + self.link_lead_address_and_contact() self.update_customer_groups() @@ -176,62 +176,24 @@ class Customer(TransactionBase): if self.lead_name: frappe.db.set_value("Lead", self.lead_name, "status", "Converted") - def create_lead_address_contact(self): + def link_lead_address_and_contact(self): if self.lead_name: - # assign lead address to customer (if already not set) - address_names = frappe.get_all('Dynamic Link', filters={ - "parenttype":"Address", - "link_doctype":"Lead", - "link_name":self.lead_name - }, fields=["parent as name"]) + # assign lead address and contact to customer (if already not set) + linked_contacts_and_addresses = frappe.get_all( + "Dynamic Link", + filters=[ + ["parenttype", "in", ["Contact", "Address"]], + ["link_doctype", "=", "Lead"], + ["link_name", "=", self.lead_name], + ], + fields=["parent as name", "parenttype as doctype"], + ) - for address_name in address_names: - address = frappe.get_doc('Address', address_name.get('name')) - if not address.has_link('Customer', self.name): - address.append('links', dict(link_doctype='Customer', link_name=self.name)) - address.save(ignore_permissions=self.flags.ignore_permissions) - - lead = frappe.db.get_value("Lead", self.lead_name, ["company_name", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True) - - if not lead.lead_name: - frappe.throw(_("Please mention the Lead Name in Lead {0}").format(self.lead_name)) - - contact_names = frappe.get_all('Dynamic Link', filters={ - "parenttype":"Contact", - "link_doctype":"Lead", - "link_name":self.lead_name - }, fields=["parent as name"]) - - for contact_name in contact_names: - contact = frappe.get_doc('Contact', contact_name.get('name')) - if not contact.has_link('Customer', self.name): - contact.append('links', dict(link_doctype='Customer', link_name=self.name)) - contact.save(ignore_permissions=self.flags.ignore_permissions) - - if not contact_names: - lead.lead_name = lead.lead_name.lstrip().split(" ") - lead.first_name = lead.lead_name[0] - lead.last_name = " ".join(lead.lead_name[1:]) - - # create contact from lead - contact = frappe.new_doc('Contact') - contact.first_name = lead.first_name - contact.last_name = lead.last_name - contact.gender = lead.gender - contact.salutation = lead.salutation - contact.email_id = lead.email_id - contact.phone = lead.phone - contact.mobile_no = lead.mobile_no - contact.is_primary_contact = 1 - contact.append('links', dict(link_doctype='Customer', link_name=self.name)) - if lead.email_id: - contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) - if lead.mobile_no: - contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) - contact.flags.ignore_permissions = self.flags.ignore_permissions - contact.autoname() - if not frappe.db.exists("Contact", contact.name): - contact.insert() + for row in linked_contacts_and_addresses: + linked_doc = frappe.get_doc(row.doctype, row.name) + if not linked_doc.has_link('Customer', self.name): + linked_doc.append('links', dict(link_doctype='Customer', link_name=self.name)) + linked_doc.save(ignore_permissions=self.flags.ignore_permissions) def validate_name_with_customer_group(self): if frappe.db.exists("Customer Group", self.name): diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 79e9e17e414..eb98e6c0bf8 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -457,12 +457,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex make_delivery_note_based_on_delivery_date() { var me = this; - var delivery_dates = []; - $.each(this.frm.doc.items || [], function(i, d) { - if(!delivery_dates.includes(d.delivery_date)) { - delivery_dates.push(d.delivery_date); - } - }); + var delivery_dates = this.frm.doc.items.map(i => i.delivery_date); + delivery_dates = [ ...new Set(delivery_dates) ]; var item_grid = this.frm.fields_dict["items"].grid; if(!item_grid.get_selected().length && delivery_dates.length > 1) { @@ -500,14 +496,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex if(!dates) return; - $.each(dates, function(i, d) { - $.each(item_grid.grid_rows || [], function(j, row) { - if(row.doc.delivery_date == d) { - row.doc.__checked = 1; - } - }); - }) - me.make_delivery_note(); + me.make_delivery_note(dates); dialog.hide(); }); dialog.show(); @@ -516,10 +505,13 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } } - make_delivery_note() { + make_delivery_note(delivery_dates) { frappe.model.open_mapped_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", - frm: this.frm + frm: this.frm, + args: { + delivery_dates + } }) } diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index cc951850a4a..0f5b1e3b89c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -565,6 +565,13 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): } if not skip_item_mapping: + def condition(doc): + # make_mapped_doc sets js `args` into `frappe.flags.args` + if frappe.flags.args and frappe.flags.args.delivery_dates: + if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates: + return False + return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + mapper["Sales Order Item"] = { "doctype": "Delivery Note Item", "field_map": { @@ -573,7 +580,7 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): "parent": "against_sales_order", }, "postprocess": update_item, - "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + "condition": condition } target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index fb86e614b6c..e1ef63578e9 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -16,7 +16,7 @@ class SellingSettings(Document): self.toggle_editable_rate_for_bundle_items() def validate(self): - for key in ["cust_master_name", "campaign_naming_by", "customer_group", "territory", + for key in ["cust_master_name", "customer_group", "territory", "maintain_same_sales_rate", "editable_price_list_rate", "selling_price_list"]: frappe.db.set_default(key, self.get(key, "")) diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py index 777b02ca66d..dd49f1355d2 100644 --- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py +++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py @@ -23,19 +23,24 @@ def execute(filters=None): row = [] outstanding_amt = get_customer_outstanding(d.name, filters.get("company"), - ignore_outstanding_sales_order=d.bypass_credit_limit_check_at_sales_order) + ignore_outstanding_sales_order=d.bypass_credit_limit_check) credit_limit = get_credit_limit(d.name, filters.get("company")) bal = flt(credit_limit) - flt(outstanding_amt) if customer_naming_type == "Naming Series": - row = [d.name, d.customer_name, credit_limit, outstanding_amt, bal, - d.bypass_credit_limit_check, d.is_frozen, - d.disabled] + row = [ + d.name, d.customer_name, credit_limit, + outstanding_amt, bal, d.bypass_credit_limit_check, + d.is_frozen, d.disabled + ] else: - row = [d.name, credit_limit, outstanding_amt, bal, - d.bypass_credit_limit_check_at_sales_order, d.is_frozen, d.disabled] + row = [ + d.name, credit_limit, outstanding_amt, bal, + d.bypass_credit_limit_check, d.is_frozen, + d.disabled + ] if credit_limit: data.append(row) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 0c0acc76e39..3e22d0fa8c8 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -68,7 +68,8 @@ def get_data(conditions, filters): (soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount, (soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount, soi.warehouse as warehouse, - so.company, soi.name + so.company, soi.name, + soi.description as description FROM `tabSales Order` so, (`tabSales Order Item` soi @@ -84,7 +85,7 @@ def get_data(conditions, filters): and so.docstatus = 1 {conditions} GROUP BY soi.name - ORDER BY so.transaction_date ASC + ORDER BY so.transaction_date ASC, soi.item_code ASC """.format(conditions=conditions), filters, as_dict=1) return data @@ -184,6 +185,12 @@ def get_columns(filters): "options": "Item", "width": 100 }) + columns.append({ + "label":_("Description"), + "fieldname": "description", + "fieldtype": "Small Text", + "width": 100 + }) columns.extend([ { diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 540aca234bd..16e38471689 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -486,7 +486,7 @@ frappe.ui.form.on(cur_frm.doctype, { "options": "Competitor Detail" }, { - "fieldtype": "Text", + "fieldtype": "Small Text", "label": __("Detailed Reason"), "fieldname": "detailed_reason" }, @@ -499,7 +499,7 @@ frappe.ui.form.on(cur_frm.doctype, { method: 'declare_enquiry_lost', args: { 'lost_reasons_list': values.lost_reason, - 'competitors': values.competitors, + 'competitors': values.competitors ? values.competitors : [], 'detailed_reason': values.detailed_reason }, callback: function(r) { diff --git a/erpnext/selling/workspace/retail/retail.json b/erpnext/selling/workspace/retail/retail.json index a851ace738c..5bce3ca648a 100644 --- a/erpnext/selling/workspace/retail/retail.json +++ b/erpnext/selling/workspace/retail/retail.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Point Of Sale\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings & Configurations\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loyalty Program\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Opening & Closing\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Point Of Sale\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings & Configurations\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loyalty Program\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening & Closing\",\"col\":4}}]", "creation": "2020-03-02 17:18:32.505616", "docstatus": 0, "doctype": "Workspace", @@ -14,7 +14,7 @@ "hidden": 0, "is_query_report": 0, "label": "Settings & Configurations", - "link_count": 0, + "link_count": 2, "onboard": 0, "type": "Card Break" }, @@ -44,7 +44,7 @@ "hidden": 0, "is_query_report": 0, "label": "Loyalty Program", - "link_count": 0, + "link_count": 2, "onboard": 0, "type": "Card Break" }, @@ -74,7 +74,7 @@ "hidden": 0, "is_query_report": 0, "label": "Opening & Closing", - "link_count": 0, + "link_count": 2, "onboard": 0, "type": "Card Break" }, @@ -101,7 +101,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:01.840989", + "modified": "2022-01-13 18:07:56.711095", "modified_by": "Administrator", "module": "Selling", "name": "Retail", @@ -110,7 +110,7 @@ "public": 1, "restrict_to_domain": "Retail", "roles": [], - "sequence_id": 22, + "sequence_id": 22.0, "shortcuts": [ { "doc_view": "", diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json index db2e6bafd55..a700ad89a32 100644 --- a/erpnext/selling/workspace/selling/selling.json +++ b/erpnext/selling/workspace/selling/selling.json @@ -5,7 +5,7 @@ "label": "Sales Order Trends" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Selling\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Sales Order Trends\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Quick Access\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Analytics\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Order Analysis\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Selling\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Items and Pricing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-01-28 11:49:12.092882", "docstatus": 0, "doctype": "Workspace", @@ -562,7 +562,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:01.990703", + "modified": "2022-01-13 17:43:02.778627", "modified_by": "Administrator", "module": "Selling", "name": "Selling", @@ -571,7 +571,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 23, + "sequence_id": 23.0, "shortcuts": [ { "color": "Grey", diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 45e8dccc319..dd185fc6636 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -213,6 +213,9 @@ erpnext.company.setup_queries = function(frm) { ["default_payroll_payable_account", {"root_type": "Liability"}], ["round_off_account", {"root_type": "Expense"}], ["write_off_account", {"root_type": "Expense"}], + ["default_deferred_expense_account", {}], + ["default_deferred_revenue_account", {}], + ["default_expense_claim_payable_account", {}], ["default_discount_account", {}], ["discount_allowed_account", {"root_type": "Expense"}], ["discount_received_account", {"root_type": "Income"}], diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index b41a01b5ef7..12e73b60d12 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -2,16 +2,18 @@ # License: GNU General Public License v3. See license.txt import copy +from urllib.parse import quote import frappe from frappe import _ -from frappe.utils import cint, cstr, nowdate +from frappe.utils import cint from frappe.utils.nestedset import NestedSet from frappe.website.utils import clear_cache from frappe.website.website_generator import WebsiteGenerator -from six.moves.urllib.parse import quote + from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder + class ItemGroup(NestedSet, WebsiteGenerator): nsm_parent_field = 'parent_item_group' website = frappe._dict( diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index bafaab814b4..1d7bad2686a 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -189,7 +189,7 @@ def add_non_standard_user_types(): user_type_limit = {} for user_type, data in user_types.items(): - user_type_limit.setdefault(frappe.scrub(user_type), 10) + user_type_limit.setdefault(frappe.scrub(user_type), 20) update_site_config('user_type_doctype_limit', user_type_limit) @@ -204,15 +204,33 @@ def get_user_types_data(): 'apply_user_permission_on': 'Employee', 'user_id_field': 'user_id', 'doctypes': { - 'Salary Slip': ['read'], + # masters + 'Holiday List': ['read'], 'Employee': ['read', 'write'], + # payroll + 'Salary Slip': ['read'], + 'Employee Benefit Application': ['read', 'write', 'create', 'delete'], + # expenses 'Expense Claim': ['read', 'write', 'create', 'delete'], + 'Employee Advance': ['read', 'write', 'create', 'delete'], + # leave and attendance 'Leave Application': ['read', 'write', 'create', 'delete'], 'Attendance Request': ['read', 'write', 'create', 'delete'], 'Compensatory Leave Request': ['read', 'write', 'create', 'delete'], + # tax 'Employee Tax Exemption Declaration': ['read', 'write', 'create', 'delete'], 'Employee Tax Exemption Proof Submission': ['read', 'write', 'create', 'delete'], - 'Timesheet': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'] + # projects + 'Timesheet': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'], + # trainings + 'Training Program': ['read'], + 'Training Feedback': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'], + # shifts + 'Shift Request': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'], + # misc + 'Employee Grievance': ['read', 'write', 'create', 'delete'], + 'Employee Referral': ['read', 'write', 'create', 'delete'], + 'Travel Request': ['read', 'write', 'create', 'delete'] } } } diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index e47837f2ca4..c5640bc0793 100644 --- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json +++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Projects Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"HR Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Support Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Shopping Cart Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Portal Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Domain Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Products Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Naming Series\",\"col\":4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Projects Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"HR Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Support Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Shopping Cart Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Portal Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Domain Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Products Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Naming Series\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Manufacturing Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Education Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Hotel Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"CRM Settings\",\"col\":3}}]", "creation": "2020-03-12 14:47:51.166455", "docstatus": 0, "doctype": "Workspace", @@ -10,7 +10,7 @@ "idx": 0, "label": "ERPNext Settings", "links": [], - "modified": "2021-11-05 21:32:55.323591", + "modified": "2022-01-13 19:18:59.362820", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", @@ -19,7 +19,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 12, + "sequence_id": 12.0, "shortcuts": [ { "icon": "project", @@ -104,13 +104,6 @@ "restrict_to_domain": "Hospitality", "type": "DocType" }, - { - "icon": "non-profit", - "label": "Healthcare Settings", - "link_to": "Healthcare Settings", - "restrict_to_domain": "Healthcare", - "type": "DocType" - }, { "icon": "setting", "label": "Domain Settings", diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index f9c585c015c..19ff2a08304 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -1,18 +1,13 @@ { "charts": [], - "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", "creation": "2020-01-23 13:46:38.833076", - "developer_mode_only": 0, - "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", - "extends_another_page": 0, "for_user": "", "hide_custom": 0, "icon": "getting-started", "idx": 0, - "is_default": 0, - "is_standard": 0, "label": "Home", "links": [ { @@ -276,18 +271,16 @@ "type": "Link" } ], - "modified": "2021-11-22 12:50:15.771366", + "modified": "2022-01-13 17:24:17.002665", "modified_by": "Administrator", "module": "Setup", "name": "Home", "owner": "Administrator", "parent_page": "", - "pin_to_bottom": 0, - "pin_to_top": 0, "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 1, + "sequence_id": 1.0, "shortcuts": [ { "label": "Item", diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 5593101575d..96751d6eae5 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -292,6 +292,7 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): join `tabStock Ledger Entry` ignore index (item_code, warehouse) on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s + and `tabStock Ledger Entry`.is_cancelled = 0 and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} group by batch_id order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC @@ -336,4 +337,4 @@ def get_pos_reserved_batch_qty(filters): ).run() flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) - return flt_reserved_batch_qty \ No newline at end of file + return flt_reserved_batch_qty diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index 8e79f0e5552..56dc71c57e1 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -33,6 +33,7 @@ "oldfieldtype": "Link", "options": "Warehouse", "read_only": 1, + "reqd": 1, "search_index": 1 }, { @@ -46,6 +47,7 @@ "oldfieldtype": "Link", "options": "Item", "read_only": 1, + "reqd": 1, "search_index": 1 }, { @@ -169,10 +171,11 @@ "idx": 1, "in_create": 1, "links": [], - "modified": "2021-03-30 23:09:39.572776", + "modified": "2022-01-30 17:04:54.715288", "modified_by": "Administrator", "module": "Stock", "name": "Bin", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -200,5 +203,6 @@ "quick_entry": 1, "search_fields": "item_code,warehouse", "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 0ef7ce29230..c34e9d05cef 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -123,7 +123,7 @@ class Bin(Document): self.db_set('projected_qty', self.projected_qty) def on_doctype_update(): - frappe.db.add_index("Bin", ["item_code", "warehouse"]) + frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse") def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False): diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py index 9c390d94b4e..250126c6b98 100644 --- a/erpnext/stock/doctype/bin/test_bin.py +++ b/erpnext/stock/doctype/bin/test_bin.py @@ -1,9 +1,36 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest +import frappe -# test_records = frappe.get_test_records('Bin') +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.utils import _create_bin +from erpnext.tests.utils import ERPNextTestCase -class TestBin(unittest.TestCase): - pass + +class TestBin(ERPNextTestCase): + + + def test_concurrent_inserts(self): + """ Ensure no duplicates are possible in case of concurrent inserts""" + item_code = "_TestConcurrentBin" + make_item(item_code) + warehouse = "_Test Warehouse - _TC" + + bin1 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse) + bin1.insert() + + bin2 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse) + with self.assertRaises(frappe.UniqueValidationError): + bin2.insert() + + # util method should handle it + bin = _create_bin(item_code, warehouse) + self.assertEqual(bin.item_code, item_code) + + frappe.db.rollback() + + def test_index_exists(self): + indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1) + if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes): + self.fail(f"Expected unique index on item-warehouse") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 70d48a42d72..d1e22440b96 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -14,6 +14,7 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.selling_controller import SellingController from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no +from erpnext.stock.utils import calculate_mapped_packed_items_return form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -128,8 +129,12 @@ class DeliveryNote(SellingController): self.validate_uom_is_integer("uom", "qty") self.validate_with_previous_doc() - from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - make_packing_list(self) + # Keeps mapped packed_items in case product bundle is updated. + if self.is_return and self.return_against: + calculate_mapped_packed_items_return(self) + else: + from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) if self._action != 'submit' and not self.is_return: set_batch_nos(self, 'warehouse', throw=True) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 4f89a19f3c7..bd18e788ba6 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -386,8 +386,7 @@ class TestDeliveryNote(ERPNextTestCase): self.assertEqual(actual_qty, 25) # return bundled item - dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1, - return_against=dn.name, qty=-2, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn1 = create_return_delivery_note(source_name=dn.name, rate=500, qty=-2) # qty after return actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") @@ -823,6 +822,15 @@ class TestDeliveryNote(ERPNextTestCase): automatically_fetch_payment_terms(enable=0) +def create_return_delivery_note(**args): + args = frappe._dict(args) + from erpnext.controllers.sales_and_purchase_return import make_return_doc + doc = make_return_doc("Delivery Note", args.source_name, None) + doc.items[0].rate = args.rate + doc.items[0].qty = args.qty + doc.submit() + return doc + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index f361c88dfc9..2a30ca11fbd 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -380,8 +380,7 @@ $.extend(erpnext.item, { // Show Stock Levels only if is_stock_item if (frm.doc.is_stock_item) { frappe.require('item-dashboard.bundle.js', function() { - frm.dashboard.parent.find('.stock-levels').remove(); - const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels'); + const section = frm.dashboard.add_section('', __("Stock Levels")); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, item_code: frm.doc.name, diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index bbbebc4097a..e71cdb37cf3 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -28,6 +28,7 @@ "standard_rate", "is_fixed_asset", "auto_create_assets", + "is_grouped_asset", "asset_category", "asset_naming_series", "over_delivery_receipt_allowance", @@ -899,6 +900,13 @@ "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission" + }, + { + "default": "0", + "depends_on": "auto_create_assets", + "fieldname": "is_grouped_asset", + "fieldtype": "Check", + "label": "Create Grouped Asset" } ], "icon": "fa fa-tag", @@ -906,7 +914,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-14 04:13:16.857534", + "modified": "2022-01-18 12:57:54.273202", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -976,6 +984,7 @@ "show_preview_popup": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "item_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index dc54c3a2c50..2c78b2c634b 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -8,6 +8,7 @@ from typing import List import frappe from frappe import _ +from frappe.model.document import Document from frappe.utils import ( cint, cstr, @@ -22,7 +23,6 @@ from frappe.utils import ( ) from frappe.utils.html_utils import clean_html from frappe.website.utils import clear_cache -from frappe.model.document import Document import erpnext from erpnext.controllers.item_variant import ( @@ -224,18 +224,20 @@ class Item(Document): self.item_code)) def add_default_uom_in_conversion_factor_table(self): - uom_conv_list = [d.uom for d in self.get("uoms")] - if self.stock_uom not in uom_conv_list: - ch = self.append('uoms', {}) - ch.uom = self.stock_uom - ch.conversion_factor = 1 + if not self.is_new() and self.has_value_changed("stock_uom"): + self.uoms = [] + frappe.msgprint( + _("Successfully changed Stock UOM, please redefine conversion factors for new UOM."), + alert=True, + ) - to_remove = [] - for d in self.get("uoms"): - if d.conversion_factor == 1 and d.uom != self.stock_uom: - to_remove.append(d) + uoms_list = [d.uom for d in self.get("uoms")] - [self.remove(d) for d in to_remove] + if self.stock_uom not in uoms_list: + self.append("uoms", { + "uom": self.stock_uom, + "conversion_factor": 1 + }) def update_website_item(self): """Update Website Item if change in Item impacts it.""" @@ -352,14 +354,6 @@ class Item(Document): frappe.throw(_("Barcode {0} is not a valid {1} code").format( item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode) - if item_barcode.barcode != item_barcode.name: - # if barcode is getting updated , the row name has to reset. - # Delete previous old row doc and re-enter row as if new to reset name in db. - item_barcode.set("__islocal", True) - item_barcode_entry_name = item_barcode.name - item_barcode.name = None - frappe.delete_doc("Item Barcode", item_barcode_entry_name) - def validate_warehouse_for_reorder(self): '''Validate Reorder level table for duplicate and conditional mandatory''' warehouse = [] diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 4028d933341..0957ce06159 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -584,6 +584,16 @@ class TestItem(ERPNextTestCase): except frappe.ValidationError as e: self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}") + def test_erasure_of_old_conversions(self): + item = create_item("_item change uom") + item.stock_uom = "Gram" + item.append("uoms", frappe._dict(uom="Box", conversion_factor=2)) + item.save() + item.reload() + item.stock_uom = "Nos" + item.save() + self.assertEqual(len(item.uoms), 1) + def test_validate_stock_item(self): self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item") diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index cd7e63b18b2..0ba97d59a14 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "REPOST-ITEM-VAL-.######", - "creation": "2020-10-22 22:27:07.742161", + "creation": "2022-01-11 15:03:38.273179", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -129,7 +129,7 @@ "reqd": 1 }, { - "default": "0", + "default": "1", "fieldname": "allow_negative_stock", "fieldtype": "Check", "label": "Allow Negative Stock" @@ -177,7 +177,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-11-24 02:18:10.524560", + "modified": "2022-01-18 10:57:33.450907", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", @@ -227,5 +227,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" -} + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index fb3b355fb74..01c5e3e4e2a 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -27,8 +27,7 @@ class RepostItemValuation(Document): self.item_code = None self.warehouse = None - self.allow_negative_stock = self.allow_negative_stock or \ - cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + self.allow_negative_stock = 1 def set_company(self): if self.based_on == "Transaction": diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 2947fafe525..ee08e38f33c 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -402,10 +402,16 @@ def update_serial_nos(sle, item_det): def get_auto_serial_nos(serial_no_series, qty): serial_nos = [] for i in range(cint(qty)): - serial_nos.append(make_autoname(serial_no_series, "Serial No")) + serial_nos.append(get_new_serial_number(serial_no_series)) return "\n".join(serial_nos) +def get_new_serial_number(series): + sr_no = make_autoname(series, "Serial No") + if frappe.db.exists("Serial No", sr_no): + sr_no = get_new_serial_number(series) + return sr_no + def auto_make_serial_nos(args): serial_nos = get_serial_nos(args.get('serial_no')) created_numbers = [] @@ -459,6 +465,13 @@ def get_serial_nos(serial_no): return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n') if s.strip()] +def clean_serial_no_string(serial_no: str) -> str: + if not serial_no: + return "" + + serial_no_list = get_serial_nos(serial_no) + return "\n".join(serial_no_list) + def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]: if args.get(field): diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 99000d12016..f8cea717251 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -8,8 +8,10 @@ import frappe from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -21,6 +23,10 @@ from erpnext.tests.utils import ERPNextTestCase class TestSerialNo(ERPNextTestCase): + + def tearDown(self): + frappe.db.rollback() + def test_cannot_create_direct(self): frappe.delete_doc_if_exists("Serial No", "_TCSER0001") @@ -176,6 +182,24 @@ class TestSerialNo(ERPNextTestCase): self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") self.assertEqual(sn_doc.purchase_document_no, se.name) + def test_auto_creation_of_serial_no(self): + """ + Test if auto created Serial No excludes existing serial numbers + """ + item_code = make_item("_Test Auto Serial Item ", { + "has_serial_no": 1, + "serial_no_series": "XYZ.###" + }).item_code + + # Reserve XYZ005 + pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005") + # XYZ005 is already used and will throw an error if used again + pr_2 = make_purchase_receipt(item_code=item_code, qty=10) + + self.assertEqual(get_serial_nos(pr_1.get("items")[0].serial_no)[0], "XYZ005") + for serial_no in get_serial_nos(pr_2.get("items")[0].serial_no): + self.assertNotEqual(serial_no, "XYZ005") + def test_serial_no_sanitation(self): "Test if Serial No input is sanitised before entering the DB." item_code = "_Test Serialized Item" @@ -192,7 +216,28 @@ class TestSerialNo(ERPNextTestCase): self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021") - frappe.db.rollback() + def test_correct_serial_no_incoming_rate(self): + """ Check correct consumption rate based on serial no record. + """ + item_code = "_Test Serialized Item" + warehouse = "_Test Warehouse - _TC" + serial_nos = ["LOWVALUATION", "HIGHVALUATION"] + + in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, + serial_no=serial_nos[0]) + in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, + serial_no=serial_nos[1]) + + out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True) + + # change serial no + out.items[0].serial_no = serial_nos[1] + out.save() + out.submit() + + value_diff = frappe.db.get_value("Stock Ledger Entry", + {"voucher_no": out.name, "voucher_type": "Delivery Note"}, + "stock_value_difference" + ) + self.assertEqual(value_diff, -113) - def tearDown(self): - frappe.db.rollback() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 93e303c9a7f..c51c9bc5f47 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -8,6 +8,7 @@ from collections import defaultdict import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Sum from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate import erpnext @@ -85,8 +86,11 @@ class StockEntry(StockController): self.validate_warehouse() self.validate_work_order() self.validate_bom() - self.mark_finished_and_scrap_items() - self.validate_finished_goods() + + if self.purpose in ("Manufacture", "Repack"): + self.mark_finished_and_scrap_items() + self.validate_finished_goods() + self.validate_with_material_request() self.validate_batch() self.validate_inspection() @@ -109,8 +113,12 @@ class StockEntry(StockController): self.set_actual_qty() self.calculate_rate_and_amount() self.validate_putaway_capacity() - self.reset_default_field_value("from_warehouse", "items", "s_warehouse") - self.reset_default_field_value("to_warehouse", "items", "t_warehouse") + + if not self.get("purpose") == "Manufacture": + # ignore scrap item wh difference and empty source/target wh + # in Manufacture Entry + self.reset_default_field_value("from_warehouse", "items", "s_warehouse") + self.reset_default_field_value("to_warehouse", "items", "t_warehouse") def on_submit(self): self.update_stock_ledger() @@ -701,26 +709,25 @@ class StockEntry(StockController): validate_bom_no(item_code, d.bom_no) def mark_finished_and_scrap_items(self): - if self.purpose in ("Repack", "Manufacture"): - if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): - return + if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): + return - finished_item = self.get_finished_item() + finished_item = self.get_finished_item() - if not finished_item and self.purpose == "Manufacture": - # In case of independent Manufacture entry, don't auto set - # user must decide and set - return + if not finished_item and self.purpose == "Manufacture": + # In case of independent Manufacture entry, don't auto set + # user must decide and set + return - for d in self.items: - if d.t_warehouse and not d.s_warehouse: - if self.purpose=="Repack" or d.item_code == finished_item: - d.is_finished_item = 1 - else: - d.is_scrap_item = 1 + for d in self.items: + if d.t_warehouse and not d.s_warehouse: + if self.purpose=="Repack" or d.item_code == finished_item: + d.is_finished_item = 1 else: - d.is_finished_item = 0 - d.is_scrap_item = 0 + d.is_scrap_item = 1 + else: + d.is_finished_item = 0 + d.is_scrap_item = 0 def get_finished_item(self): finished_item = None @@ -733,9 +740,9 @@ class StockEntry(StockController): def validate_finished_goods(self): """ - 1. Check if FG exists - 2. Check if Multiple FG Items are present - 3. Check FG Item and Qty against WO if present + 1. Check if FG exists (mfg, repack) + 2. Check if Multiple FG Items are present (mfg) + 3. Check FG Item and Qty against WO if present (mfg) """ production_item, wo_qty, finished_items = None, 0, [] @@ -748,8 +755,9 @@ class StockEntry(StockController): for d in self.get('items'): if d.is_finished_item: if not self.work_order: + # Independent MFG Entry/ Repack Entry, no WO to match against finished_items.append(d.item_code) - continue # Independent Manufacture Entry, no WO to match against + continue if d.item_code != production_item: frappe.throw(_("Finished Item {0} does not match with Work Order {1}") @@ -762,19 +770,17 @@ class StockEntry(StockController): finished_items.append(d.item_code) - if len(set(finished_items)) > 1: + if not finished_items: frappe.throw( - msg=_("Multiple items cannot be marked as finished item"), - title=_("Note"), - exc=FinishedGoodError + msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), + title=_("Missing Finished Good"), exc=FinishedGoodError ) if self.purpose == "Manufacture": - if not finished_items: + if len(set(finished_items)) > 1: frappe.throw( - msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), - title=_("Missing Finished Good"), - exc=FinishedGoodError + msg=_("Multiple items cannot be marked as finished item"), + title=_("Note"), exc=FinishedGoodError ) allowance_percentage = flt( @@ -1275,22 +1281,29 @@ class StockEntry(StockController): if not self.pro_doc: self.set_work_order_details() - scrap_items = frappe.db.sql(''' - SELECT - JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description - FROM - `tabJob Card` JC, `tabJob Card Scrap Item` JCSI - WHERE - JCSI.parent = JC.name AND JC.docstatus = 1 - AND JCSI.item_code IS NOT NULL AND JC.work_order = %s - GROUP BY - JCSI.item_code - ''', self.work_order, as_dict=1) - - pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty) - if pending_qty <=0: + if not self.pro_doc.operations: return [] + job_card = frappe.qb.DocType('Job Card') + job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item') + + scrap_items = ( + frappe.qb.from_(job_card) + .select( + Sum(job_card_scrap_item.stock_qty).as_('stock_qty'), + job_card_scrap_item.item_code, job_card_scrap_item.item_name, + job_card_scrap_item.description, job_card_scrap_item.stock_uom) + .join(job_card_scrap_item) + .on(job_card_scrap_item.parent == job_card.name) + .where( + (job_card_scrap_item.item_code.isnotnull()) + & (job_card.work_order == self.work_order) + & (job_card.docstatus == 1)) + .groupby(job_card_scrap_item.item_code) + ).run(as_dict=1) + + pending_qty = flt(self.get_completed_job_card_qty()) - flt(self.pro_doc.produced_qty) + used_scrap_items = self.get_used_scrap_items() for row in scrap_items: row.stock_qty -= flt(used_scrap_items.get(row.item_code)) @@ -1304,6 +1317,9 @@ class StockEntry(StockController): return scrap_items + def get_completed_job_card_qty(self): + return flt(min([d.completed_qty for d in self.pro_doc.operations])) + def get_used_scrap_items(self): used_scrap_items = defaultdict(float) data = frappe.get_all( @@ -1429,14 +1445,15 @@ class StockEntry(StockController): qty = req_qty_each * flt(self.fg_completed_qty) elif backflushed_materials.get(item.item_code): + precision = frappe.get_precision("Stock Entry Detail", "qty") for d in backflushed_materials.get(item.item_code): - if d.get(item.warehouse): + if d.get(item.warehouse) > 0: if (qty > req_qty): - qty = (qty/trans_qty) * flt(self.fg_completed_qty) + qty = ((flt(qty, precision) - flt(d.get(item.warehouse), precision)) + / (flt(trans_qty, precision) - flt(produced_qty, precision)) + ) * flt(self.fg_completed_qty) - if consumed_qty and frappe.db.get_single_value("Manufacturing Settings", - "material_consumption"): - qty -= consumed_qty + d[item.warehouse] -= qty if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): qty = frappe.utils.ceil(qty) @@ -1656,6 +1673,8 @@ class StockEntry(StockController): for d in self.get("items"): item_code = d.get('original_item') or d.get('item_code') reserve_warehouse = item_wh.get(item_code) + if not (reserve_warehouse and item_code): + continue stock_bin = get_bin(item_code, reserve_warehouse) stock_bin.update_reserved_qty_for_sub_contracting() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index b874874adfb..306f2c3e69f 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -226,9 +226,47 @@ class TestStockEntry(ERPNextTestCase): mtn.cancel() - def test_repack_no_change_in_valuation(self): - company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') + def test_repack_multiple_fg(self): + "Test `is_finished_item` for one item repacked into two items." + make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100) + repack = frappe.copy_doc(test_records[3]) + repack.posting_date = nowdate() + repack.posting_time = nowtime() + + repack.items[0].qty = 100.0 + repack.items[0].transfer_qty = 100.0 + repack.items[1].qty = 50.0 + + repack.append("items", { + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - _TC", + "basic_rate": 150, + "item_code": "_Test Item 2", + "parentfield": "items", + "qty": 50.0, + "stock_uom": "_Test UOM", + "t_warehouse": "_Test Warehouse - _TC", + "transfer_qty": 50.0, + "uom": "_Test UOM" + }) + repack.set_stock_entry_type() + repack.insert() + + self.assertEqual(repack.items[1].is_finished_item, 1) + self.assertEqual(repack.items[2].is_finished_item, 1) + + repack.items[1].is_finished_item = 0 + repack.items[2].is_finished_item = 0 + + # must raise error if 0 fg in repack entry + self.assertRaises(FinishedGoodError, repack.validate_finished_goods) + + repack.delete() # teardown + + def test_repack_no_change_in_valuation(self): make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100) make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=50, basic_rate=100) @@ -813,6 +851,34 @@ class TestStockEntry(ERPNextTestCase): self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].amount, 0) + def test_zero_incoming_rate(self): + """ Make sure incoming rate of 0 is allowed while consuming. + + qty | rate | valuation rate + 1 | 100 | 100 + 1 | 0 | 50 + -1 | 100 | 0 + -1 | 0 <--- assert this + """ + item_code = "_TestZeroVal" + warehouse = "_Test Warehouse - _TC" + create_item('_TestZeroVal') + _receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100) + receipt2 = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True) + receipt2.items[0].allow_zero_valuation_rate = 1 + receipt2.save() + receipt2.submit() + + issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) + + value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + self.assertEqual(value_diff, -100) + + issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) + value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + self.assertEqual(value_diff, 0) + + def test_gle_for_opening_stock_entry(self): mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=50, basic_rate=100, diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index cafbd7581ce..a1030d54964 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -5,7 +5,10 @@ import frappe from frappe.core.page.permission_manager.permission_manager import reset from frappe.utils import add_days, today -from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.delivery_note.test_delivery_note import ( + create_delivery_note, + create_return_delivery_note, +) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( create_landed_cost_voucher, @@ -232,8 +235,7 @@ class TestStockLedgerEntry(ERPNextTestCase): self.assertEqual(outgoing_rate, 100) # Return Entry: Qty = -2, Rate = 150 - return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150, - company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + return_dn = create_return_delivery_note(source_name=dn.name, rate=150, qty=-2) # check incoming rate for Return entry incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index c4ddc9e2d6f..428370cc758 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -6,7 +6,7 @@ import frappe -from frappe.utils import add_days, flt, nowdate, nowtime, random_string +from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string from erpnext.accounts.utils import get_stock_and_account_balance from erpnext.stock.doctype.item.test_item import create_item @@ -439,8 +439,8 @@ class TestStockReconciliation(ERPNextTestCase): self.assertRaises(frappe.ValidationError, sr.submit) def test_serial_no_cancellation(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1) if not item.has_serial_no: item.has_serial_no = 1 @@ -466,6 +466,31 @@ class TestStockReconciliation(ERPNextTestCase): self.assertEqual(len(active_sr_no), 10) + def test_serial_no_creation_and_inactivation(self): + item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1) + if not item.has_serial_no: + item.has_serial_no = 1 + item.save() + + item_code = item.name + warehouse = "_Test Warehouse - _TC" + + sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, + serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100) + sr.save() + self.assertEqual(cstr(sr.items[0].current_serial_no), "") + sr.submit() + + active_sr_no = frappe.get_all("Serial No", + filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + self.assertEqual(len(active_sr_no), 1) + + sr.cancel() + active_sr_no = frappe.get_all("Serial No", + filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + self.assertEqual(len(active_sr_no), 0) + + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) if not batch_item_doc.has_batch_no: diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index 44e13869ddb..87097c72fa4 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -55,7 +55,8 @@ def get_stock_ledger_entries(filters): return frappe.db.sql("""select item_code, batch_no, warehouse, posting_date, actual_qty from `tabStock Ledger Entry` - where docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % + where is_cancelled = 0 + and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % conditions, as_dict=1) def get_item_warehouse_batch_map(filters, float_precision): diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py index 5f6184d6f35..058af77aa21 100644 --- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -91,7 +91,7 @@ def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDLis voucher_nos = [fe.get('voucher_no') for fe in filtered_entries] svd_list = frappe.get_list( 'Stock Ledger Entry', fields=['item_code','stock_value_difference'], - filters=[('voucher_no', 'in', voucher_nos)] + filters=[('voucher_no', 'in', voucher_nos), ("is_cancelled", "=", 0)] ) assign_item_groups_to_svd_list(svd_list) return svd_list diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index 3f490653e14..cfa1e474c7b 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -76,6 +76,7 @@ def get_consumed_items(condition): on sle.voucher_no = se.name where actual_qty < 0 + and is_cancelled = 0 and voucher_type not in ('Delivery Note', 'Sales Invoice') %s group by item_code""" % condition, as_dict=1) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index fe2417bba7e..ef7c2cc7d9e 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -86,10 +86,10 @@ frappe.query_reports["Stock Ledger"] = { ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.fieldname == "out_qty" && data.out_qty < 0) { + if (column.fieldname == "out_qty" && data && data.out_qty < 0) { value = "" + value + ""; } - else if (column.fieldname == "in_qty" && data.in_qty > 0) { + else if (column.fieldname == "in_qty" && data && data.in_qty > 0) { value = "" + value + ""; } diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index 1dcf863a9d0..525af40b412 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -1,6 +1,8 @@ import unittest from typing import List, Tuple +import frappe + from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report DEFAULT_FILTERS = { @@ -10,8 +12,12 @@ DEFAULT_FILTERS = { } +batch = frappe.db.get_value("Batch", fieldname=["name"], as_dict=True, order_by="creation desc") + REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ ("Stock Ledger", {"_optional": True}), + ("Stock Ledger", {"batch_no": batch}), + ("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}), ("Stock Balance", {"_optional": True}), ("Stock Projected Qty", {"_optional": True}), ("Batch-Wise Balance History", {}), @@ -40,6 +46,13 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ ("Item Variant Details", {"item": "_Test Variant Item",}), ("Total Stock Summary", {"group_by": "warehouse",}), ("Batch Item Expiry Status", {}), + ("Incorrect Stock Value Report", {"company": "_Test Company with perpetual inventory"}), + ("Incorrect Serial No Valuation", {}), + ("Incorrect Balance Qty After Transaction", {}), + ("Supplier-Wise Sales Analytics", {}), + ("Item Prices", {"items": "Enabled Items only"}), + ("Delayed Item Report", {"based_on": "Sales Invoice"}), + ("Delayed Item Report", {"based_on": "Delivery Note"}), ("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}), ("Stock Ledger Invariant Check", { diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 107bb23222e..0a7ab4009c0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -105,6 +105,7 @@ def get_args_for_future_sle(row): def validate_serial_no(sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + for sn in get_serial_nos(sle.serial_no): args = copy.deepcopy(sle) args.serial_no = sn @@ -423,6 +424,8 @@ class update_entries_after(object): return sorted(entries_to_fix, key=lambda k: k['timestamp']) def process_sle(self, sle): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + # previous sle data for this warehouse self.wh_data = self.data[sle.warehouse] @@ -437,7 +440,7 @@ class update_entries_after(object): if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) - if sle.serial_no: + if get_serial_nos(sle.serial_no): self.get_serialized_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) if sle.voucher_type == "Stock Reconciliation": @@ -449,8 +452,9 @@ class update_entries_after(object): # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction - self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]] self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + if self.valuation_method != "Moving Average": + self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]] else: if self.valuation_method == "Moving Average": self.get_moving_average_values(sle) @@ -602,9 +606,9 @@ class update_entries_after(object): incoming_rate = self.wh_data.valuation_rate stock_value_change = 0 - if incoming_rate: + if actual_qty > 0: stock_value_change = actual_qty * incoming_rate - elif actual_qty < 0: + else: # In case of delivery/stock issue, get average purchase rate # of serial nos of current entry if not sle.is_cancelled: @@ -646,6 +650,7 @@ class update_entries_after(object): where company = %s and actual_qty > 0 + and is_cancelled = 0 and (serial_no = %s or serial_no like %s or serial_no like %s @@ -901,6 +906,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, item_code = %s AND warehouse = %s AND valuation_rate >= 0 + AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type)) @@ -911,6 +917,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, where item_code = %s AND valuation_rate > 0 + AND is_cancelled = 0 AND NOT(voucher_no = %s AND voucher_type = %s) order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type)) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 3c70b41eda7..7c63c17ad03 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -103,7 +103,7 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None serial_nos = get_serial_nos_data_after_transactions(args) return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) - if last_entry else (0.0, 0.0, 0.0)) + if last_entry else (0.0, 0.0, None)) else: return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) else: @@ -176,13 +176,7 @@ def get_latest_stock_balance(): def get_bin(item_code, warehouse): bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) if not bin: - bin_obj = frappe.get_doc({ - "doctype": "Bin", - "item_code": item_code, - "warehouse": warehouse, - }) - bin_obj.flags.ignore_permissions = 1 - bin_obj.insert() + bin_obj = _create_bin(item_code, warehouse) else: bin_obj = frappe.get_doc('Bin', bin, for_update=True) bin_obj.flags.ignore_permissions = True @@ -192,16 +186,24 @@ def get_or_make_bin(item_code: str , warehouse: str) -> str: bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) if not bin_record: - bin_obj = frappe.get_doc({ - "doctype": "Bin", - "item_code": item_code, - "warehouse": warehouse, - }) + bin_obj = _create_bin(item_code, warehouse) + bin_record = bin_obj.name + return bin_record + +def _create_bin(item_code, warehouse): + """Create a bin and take care of concurrent inserts.""" + + bin_creation_savepoint = "create_bin" + try: + frappe.db.savepoint(bin_creation_savepoint) + bin_obj = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse) bin_obj.flags.ignore_permissions = 1 bin_obj.insert() - bin_record = bin_obj.name + except frappe.UniqueValidationError: + frappe.db.rollback(save_point=bin_creation_savepoint) # preserve transaction in postgres + bin_obj = frappe.get_last_doc("Bin", {"item_code": item_code, "warehouse": warehouse}) - return bin_record + return bin_obj def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): """WARNING: This function is deprecated. Inline this function instead of using it.""" @@ -419,6 +421,19 @@ def is_reposting_item_valuation_in_progress(): if reposting_in_progress: frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) + +def calculate_mapped_packed_items_return(return_doc): + parent_items = set([item.parent_item for item in return_doc.packed_items]) + against_doc = frappe.get_doc(return_doc.doctype, return_doc.return_against) + + for original_bundle, returned_bundle in zip(against_doc.items, return_doc.items): + if original_bundle.item_code in parent_items: + for returned_packed_item, original_packed_item in zip(return_doc.packed_items, against_doc.packed_items): + if returned_packed_item.parent_item == original_bundle.item_code: + returned_packed_item.parent_detail_docname = returned_bundle.name + returned_packed_item.qty = (original_packed_item.qty / original_bundle.qty) * returned_bundle.qty + + def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool: """Check if there are pending reposting job till the specified posting date.""" diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json index 4df27f5dbff..ed33067e733 100644 --- a/erpnext/stock/workspace/stock/stock.json +++ b/erpnext/stock/workspace/stock/stock.json @@ -1,10 +1,11 @@ { "charts": [ { - "chart_name": "Warehouse wise Stock Value" + "chart_name": "Warehouse wise Stock Value", + "label": "Warehouse wise Stock Value" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Stock\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Quick Access\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Material Request\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Receipt\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Delivery Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Ledger\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Balance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Masters & Reports\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Items and Pricing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Stock Transactions\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Stock Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Serial No and Batch\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Incorrect Data Report\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Stock\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Warehouse wise Stock Value\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Receipt\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Delivery Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Masters & Reports\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock Transactions\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Serial No and Batch\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-03-02 15:43:10.096528", "docstatus": 0, "doctype": "Workspace", @@ -706,7 +707,7 @@ "type": "Link" } ], - "modified": "2021-11-23 04:34:00.420870", + "modified": "2022-01-13 17:47:38.339931", "modified_by": "Administrator", "module": "Stock", "name": "Stock", @@ -715,7 +716,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 24, + "sequence_id": 24.0, "shortcuts": [ { "color": "Green", diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index bfbffe22ad7..4dbb0e7e86f 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -111,6 +111,7 @@ frappe.ui.form.on('Service Level Agreement', { filters: [ ['DocType', 'issingle', '=', 0], ['DocType', 'istable', '=', 0], + ['DocType', 'is_submittable', '=', 0], ['DocType', 'name', 'not in', invalid_doctypes], ['DocType', 'module', 'not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] ] diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index ea617fd1eb5..526b6aa249e 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -29,6 +29,7 @@ from erpnext.support.doctype.issue.issue import get_holidays class ServiceLevelAgreement(Document): def validate(self): + self.validate_selected_doctype() self.validate_doc() self.validate_status_field() self.check_priorities() @@ -106,6 +107,23 @@ class ServiceLevelAgreement(Document): frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format( frappe.bold(self.entity_type), frappe.bold(self.entity))) + def validate_selected_doctype(self): + invalid_doctypes = list(frappe.model.core_doctypes_list) + invalid_doctypes.extend(['Cost Center', 'Company']) + valid_document_types = frappe.get_all('DocType', { + 'issingle': 0, + 'istable': 0, + 'is_submittable': 0, + 'name': ['not in', invalid_doctypes], + 'module': ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] + }, pluck="name") + + if self.document_type not in valid_document_types: + frappe.throw( + msg=_("Please select valid document type."), + title=_("Invalid Document Type") + ) + def validate_status_field(self): meta = frappe.get_meta(self.document_type) if not meta.get_field("status"): @@ -247,9 +265,15 @@ def get_active_service_level_agreement_for(doc): ] customer = doc.get('customer') - or_filters.append( - ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)] - ) + if customer: + or_filters.extend([ + ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)], + ["Service Level Agreement", "entity_type", "is", "not set"] + ]) + else: + or_filters.append( + ["Service Level Agreement", "entity_type", "is", "not set"] + ) default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter, @@ -361,11 +385,18 @@ def apply(doc, method=None): sla = get_active_service_level_agreement_for(doc) if not sla: + remove_sla_if_applied(doc) return process_sla(doc, sla) +def remove_sla_if_applied(doc): + doc.service_level_agreement = None + doc.response_by = None + doc.resolution_by = None + + def process_sla(doc, sla): if not doc.creation: @@ -670,7 +701,7 @@ def on_communication_update(doc, status): update_response_and_resolution_metrics(parent, for_resolution) update_agreement_status(parent, for_resolution) - parent.save() + parent.save(ignore_permissions=True) def reset_expected_response_and_resolution(doc): diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py deleted file mode 100644 index 22e2c374e12..00000000000 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py +++ /dev/null @@ -1,13 +0,0 @@ -from frappe import _ - - -def get_data(): - return { - 'fieldname': 'service_level_agreement', - 'transactions': [ - { - 'label': _('Issue'), - 'items': ['Issue'] - } - ] - } diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index b07c862c7b0..a34124fba20 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -244,6 +244,13 @@ class TestServiceLevelAgreement(unittest.TestCase): applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement') self.assertEqual(applied_sla, lead_sla.name) + # check if SLA is removed if condition fails + lead.reload() + lead.source = None + lead.save() + applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement') + self.assertFalse(applied_sla) + def tearDown(self): for d in frappe.get_all("Service Level Agreement"): frappe.delete_doc("Service Level Agreement", d.name, force=1) diff --git a/erpnext/support/workspace/support/support.json b/erpnext/support/workspace/support/support.json index d68c7c70cf7..8ca3a676c92 100644 --- a/erpnext/support/workspace/support/support.json +++ b/erpnext/support/workspace/support/support.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Issue\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Maintenance Visit\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Service Level Agreement\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Issues\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Maintenance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Service Level Agreement\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Warranty\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Issue\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Maintenance Visit\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Service Level Agreement\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Issues\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Service Level Agreement\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Warranty\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "creation": "2020-03-02 15:48:23.224699", "docstatus": 0, "doctype": "Workspace", @@ -169,7 +169,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:02.699924", + "modified": "2022-01-13 17:48:27.247406", "modified_by": "Administrator", "module": "Support", "name": "Support", @@ -178,7 +178,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 25, + "sequence_id": 25.0, "shortcuts": [ { "color": "Yellow", diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index bc9f04e0892..2bd7e9e71d0 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -92,6 +92,8 @@ def change_settings(doctype, settings_dict): for key, value in settings_dict.items(): setattr(settings, key, value) settings.save() + # singles are cached by default, clear to avoid flake + frappe.db.value_cache[settings] = {} yield # yield control to calling function finally: diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 1d8b3a8db67..feea2284b78 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -181,8 +181,6 @@ class TransactionBase(StatusUpdater): if len(child_table_values) > 1: self.set(default_field, None) - else: - self.set(default_field, list(child_table_values)[0]) def delete_events(ref_type, ref_name): events = frappe.db.sql_list(""" SELECT diff --git a/erpnext/utilities/workspace/utilities/utilities.json b/erpnext/utilities/workspace/utilities/utilities.json index 02a8af5d6c9..5b81e039b14 100644 --- a/erpnext/utilities/workspace/utilities/utilities.json +++ b/erpnext/utilities/workspace/utilities/utilities.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Video\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Video\",\"col\":4}}]", "creation": "2020-09-10 12:21:22.335307", "docstatus": 0, "doctype": "Workspace", @@ -40,7 +40,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:03.350805", + "modified": "2022-01-13 17:50:10.067510", "modified_by": "Administrator", "module": "Utilities", "name": "Utilities", @@ -49,7 +49,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 30, + "sequence_id": 30.0, "shortcuts": [], "title": "Utilities" } \ No newline at end of file