diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..be425ec2d9 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,12 @@ +# Since version 2.23 (released in August 2019), git-blame has a feature +# to ignore or bypass certain commits. +# +# This file contains a list of commits that are not likely what you +# are looking for in a blame, such as mass reformatting or renaming. +# You can set this file as a default ignore file for blame by running +# the following command. +# +# $ git config blame.ignoreRevsFile .git-blame-ignore-revs + +# This commit just changes spaces to tabs for indentation in some files +5f473611bd6ed57703716244a054d3fb5ba9cd23 diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml new file mode 100644 index 0000000000..7c9e0272c9 --- /dev/null +++ b/.github/workflows/patch.yml @@ -0,0 +1,69 @@ +name: Patch + +on: [pull_request, workflow_dispatch] + +jobs: + test: + runs-on: ubuntu-18.04 + + name: Patch Test + + services: + mysql: + image: mariadb:10.3 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: YES + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.6 + + - name: Add to Hosts + run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install + run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + + - name: Run Patch Tests + run: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/server-tests.yml similarity index 59% rename from .github/workflows/ci-tests.yml rename to .github/workflows/server-tests.yml index 84ecfb1457..92685e2177 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/server-tests.yml @@ -1,6 +1,6 @@ -name: CI +name: Server -on: [pull_request, workflow_dispatch, push] +on: [pull_request, workflow_dispatch] jobs: test: @@ -10,15 +10,9 @@ jobs: fail-fast: false matrix: - include: - - TYPE: "server" - JOB_NAME: "Server" - RUN_COMMAND: cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext --coverage - - TYPE: "patch" - JOB_NAME: "Patch" - RUN_COMMAND: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate + container: [1, 2, 3] - name: ${{ matrix.JOB_NAME }} + name: Python Unit Tests services: mysql: @@ -36,7 +30,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: 3.7 - name: Add to Hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts @@ -49,6 +43,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- + - name: Cache node modules uses: actions/cache@v2 env: @@ -60,6 +55,7 @@ jobs: ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- + - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" @@ -76,33 +72,39 @@ jobs: run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh - name: Run Tests - run: ${{ matrix.RUN_COMMAND }} + run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage env: - TYPE: ${{ matrix.TYPE }} + TYPE: server + CI_BUILD_ID: ${{ github.run_id }} + ORCHESTRATOR_URL: http://test-orchestrator.frappe.io - - name: Coverage - Pull Request - if: matrix.TYPE == 'server' && github.event_name == 'pull_request' + - name: Upload Coverage Data run: | cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE} cd ${GITHUB_WORKSPACE} - pip install coveralls==2.2.0 - pip install coverage==4.5.4 - coveralls --service=github + pip3 install coverage==5.5 + pip3 install coveralls==3.0.1 + coveralls env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} - COVERALLS_SERVICE_NAME: github - - - name: Coverage - Push - if: matrix.TYPE == 'server' && github.event_name == 'push' - run: | - cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE} - cd ${GITHUB_WORKSPACE} - pip install coveralls==2.2.0 - pip install coverage==4.5.4 - coveralls --service=github-actions - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} - COVERALLS_SERVICE_NAME: github-actions + COVERALLS_FLAG_NAME: run-${{ matrix.container }} + COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }} + COVERALLS_PARALLEL: true + coveralls: + name: Coverage Wrap Up + needs: test + container: python:3-slim + runs-on: ubuntu-18.04 + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Coveralls Finished + run: | + cd ${GITHUB_WORKSPACE} + pip3 install coverage==5.5 + pip3 install coveralls==3.0.1 + coveralls --finish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index fc1d7e344a..e657a9ae34 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -7,7 +7,8 @@ import frappe import unittest from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import delete_accounting_dimension + +test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department'] class TestAccountingDimension(unittest.TestCase): def setUp(self): diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index 7877abd026..7f6254f99f 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -9,6 +9,8 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError +test_dependencies = ['Location', 'Cost Center', 'Department'] + class TestAccountingDimensionFilter(unittest.TestCase): def setUp(self): create_dimension() diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py index 10cd939894..dc472c7695 100644 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py @@ -10,6 +10,8 @@ from erpnext.accounts.general_ledger import ClosedAccountingPeriod from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +test_dependencies = ['Item'] + class TestAccountingPeriod(unittest.TestCase): def test_overlap(self): ap1 = create_accounting_period(start_date = "2018-04-01", @@ -38,7 +40,7 @@ def create_accounting_period(**args): accounting_period.start_date = args.start_date or nowdate() accounting_period.end_date = args.end_date or add_months(nowdate(), 1) accounting_period.company = args.company or "_Test Company" - accounting_period.period_name =args.period_name or "_Test_Period_Name_1" + accounting_period.period_name = args.period_name or "_Test_Period_Name_1" accounting_period.append("closed_documents", { "document_type": 'Sales Invoice', "closed": 1 }) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 781f94e203..2735b1ccee 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -18,6 +18,7 @@ "delete_linked_ledger_entries", "book_asset_depreciation_entry_automatically", "unlink_advance_payment_on_cancelation_of_order", + "post_change_gl_entries", "tax_settings_section", "determine_address_tax_category_from", "column_break_19", @@ -253,6 +254,12 @@ { "fieldname": "column_break_19", "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "post_change_gl_entries", + "fieldtype": "Check", + "label": "Post Ledger Entries for Given Change" } ], "icon": "icon-cog", @@ -260,7 +267,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-30 15:25:10.381008", + "modified": "2021-05-25 12:34:05.858669", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 059e1d3158..19041a3f73 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -120,4 +120,4 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { plaid_success(token, response) { frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); } -}; +}; \ No newline at end of file diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index c5ec23c829..603e21ea24 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -11,6 +11,8 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_pur from erpnext.accounts.doctype.budget.budget import get_actual_expense, BudgetError from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +test_dependencies = ['Monthly Distribution'] + class TestBudget(unittest.TestCase): def test_monthly_budget_crossed_ignore(self): set_total_expense_zero(nowdate(), "cost_center") diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index ef44626b37..3b764aab10 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -293,7 +293,7 @@ def validate_accounts(file_name): accounts_dict = {} for account in accounts: accounts_dict.setdefault(account["account_name"], account) - if not hasattr(account, "parent_account"): + if "parent_account" not in account: msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.") msg += "

" msg += _("Alternatively, you can download the template and fill your data in.") diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index c5ce514cdd..e2d4d82e41 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -29,7 +29,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44) self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44) self.assertEqual(round(amounts.get('grand_total'), 2), 120.44) - + def test_gl_entries(self): dunning = create_dunning() dunning.submit() diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 7b62b617f9..95d2ee56d9 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -49,11 +49,11 @@ class InvoiceDiscounting(AccountsController): self.make_gl_entries() def on_cancel(self): - self.set_status() + self.set_status(cancel=1) self.update_sales_invoice() self.make_gl_entries() - def set_status(self, status=None): + def set_status(self, status=None, cancel=0): if status: self.status = status self.db_set("status", status) @@ -66,6 +66,9 @@ class InvoiceDiscounting(AccountsController): elif self.docstatus == 2: self.status = "Cancelled" + if cancel: + self.db_set('status', self.status, update_modified = True) + def update_sales_invoice(self): for d in self.invoices: if self.docstatus == 1: diff --git a/erpnext/accounts/doctype/journal_entry/regional/india.js b/erpnext/accounts/doctype/journal_entry/regional/india.js new file mode 100644 index 0000000000..75a69ac0cf --- /dev/null +++ b/erpnext/accounts/doctype/journal_entry/regional/india.js @@ -0,0 +1,17 @@ +frappe.ui.form.on("Journal Entry", { + refresh: function(frm) { + frm.set_query('company_address', function(doc) { + if(!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + query: 'frappe.contacts.doctype.address.address.address_query', + filters: { + link_doctype: 'Company', + link_name: doc.company + } + }; + }); + } +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 62ab76c323..e01c651a93 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -65,7 +65,6 @@ class PaymentEntry(AccountsController): self.set_status() def on_submit(self): - self.setup_party_account_field() if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) self.make_gl_entries() @@ -78,7 +77,6 @@ class PaymentEntry(AccountsController): def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') - self.setup_party_account_field() self.make_gl_entries(cancel=1) self.update_outstanding_amounts() self.update_advance_paid() @@ -122,6 +120,11 @@ class PaymentEntry(AccountsController): if flt(d.allocated_amount) > flt(d.outstanding_amount): frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)) + # Check for negative outstanding invoices as well + if flt(d.allocated_amount) < 0: + if flt(d.allocated_amount) < flt(d.outstanding_amount): + frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)) + def delink_advance_entry_references(self): for reference in self.references: if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"): @@ -177,7 +180,7 @@ class PaymentEntry(AccountsController): for field, value in iteritems(ref_details): if field == 'exchange_rate' or not d.get(field) or force: - d.set(field, value) + d.db_set(field, value) def validate_payment_type(self): if self.payment_type not in ("Receive", "Pay", "Internal Transfer"): @@ -386,6 +389,8 @@ class PaymentEntry(AccountsController): else: self.status = 'Draft' + self.db_set('status', self.status, update_modified = True) + def set_amounts(self): self.set_amounts_in_company_currency() self.set_total_allocated_amount() @@ -791,7 +796,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices): outstanding_invoices.pop(idx - 1) outstanding_invoices += invoice_ref_based_on_payment_terms[idx] - + return outstanding_invoices def get_orders_to_be_billed(posting_date, party_type, party, @@ -989,6 +994,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre outstanding_amount = ref_doc.get("outstanding_amount") elif reference_doctype == "Donation": total_amount = ref_doc.get("amount") + outstanding_amount = total_amount exchange_rate = 1 elif reference_doctype == "Dunning": total_amount = ref_doc.get("dunning_amount") diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 53ac996290..468978785b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -101,7 +101,7 @@ class PaymentRequest(Document): controller.validate_transaction_currency(self.currency) controller.request_for_payment(**payment_record) - + def get_request_amount(self): data_of_completed_requests = frappe.get_all("Integration Request", filters={ 'reference_doctype': self.doctype, @@ -492,7 +492,6 @@ def update_payment_req_status(doc, method): status = 'Requested' pay_req_doc.db_set('status', status) - frappe.db.commit() def get_dummy_message(doc): return frappe.render_template("""{% if doc.contact_person -%} diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json index 47546c07a4..84c941ecc1 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json @@ -1,350 +1,138 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "ACC-PCV-.YYYY.-.#####", - "beta": 0, - "creation": "2013-01-10 16:34:07", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "autoname": "ACC-PCV-.YYYY.-.#####", + "creation": "2013-01-10 16:34:07", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "transaction_date", + "posting_date", + "fiscal_year", + "amended_from", + "company", + "cost_center_wise_pnl", + "column_break1", + "closing_account_head", + "remarks" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "transaction_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": "Transaction Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "transaction_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "transaction_date", + "fieldtype": "Date", + "label": "Transaction Date", + "oldfieldname": "transaction_date", + "oldfieldtype": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "posting_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": "Posting Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "posting_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "oldfieldname": "posting_date", + "oldfieldtype": "Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "fiscal_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Closing Fiscal Year", - "length": 0, - "no_copy": 0, - "oldfieldname": "fiscal_year", - "oldfieldtype": "Select", - "options": "Fiscal Year", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "fiscal_year", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Closing Fiscal Year", + "oldfieldname": "fiscal_year", + "oldfieldtype": "Select", + "options": "Fiscal Year", + "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": 1, - "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, - "oldfieldname": "amended_from", - "oldfieldtype": "Data", - "options": "Period Closing Voucher", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "oldfieldname": "amended_from", + "oldfieldtype": "Data", + "options": "Period Closing Voucher", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "oldfieldname": "company", - "oldfieldtype": "Select", - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "oldfieldname": "company", + "oldfieldtype": "Select", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break1", - "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, - "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break1", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "The account head under Liability or Equity, in which Profit/Loss will be booked", - "fieldname": "closing_account_head", - "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": "Closing Account Head", - "length": 0, - "no_copy": 0, - "oldfieldname": "closing_account_head", - "oldfieldtype": "Link", - "options": "Account", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "description": "The account head under Liability or Equity, in which Profit/Loss will be booked", + "fieldname": "closing_account_head", + "fieldtype": "Link", + "label": "Closing Account Head", + "oldfieldname": "closing_account_head", + "oldfieldtype": "Link", + "options": "Account", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "remarks", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Remarks", - "length": 0, - "no_copy": 0, - "oldfieldname": "remarks", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "remarks", + "fieldtype": "Small Text", + "label": "Remarks", + "oldfieldname": "remarks", + "oldfieldtype": "Small Text", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "cost_center_wise_pnl", + "fieldtype": "Check", + "label": "Book Cost Center Wise Profit/Loss" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-file-text", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Period Closing Voucher", - "owner": "Administrator", + ], + "icon": "fa fa-file-text", + "idx": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-05-20 15:27:37.210458", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Period Closing Voucher", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "posting_date, fiscal_year", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "closing_account_head", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "search_fields": "posting_date, fiscal_year", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "closing_account_head" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index a74fa062b6..b0a5b04de6 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -51,63 +51,96 @@ class PeriodClosingVoucher(AccountsController): def make_gl_entries(self): gl_entries = [] - net_pl_balance = 0 - dimension_fields = ['t1.cost_center'] + net_pl_balance = 0 - accounting_dimensions = get_accounting_dimensions() - for dimension in accounting_dimensions: - dimension_fields.append('t1.{0}'.format(dimension)) - - dimension_filters, default_dimensions = get_dimensions() - - pl_accounts = self.get_pl_balances(dimension_fields) + pl_accounts = self.get_pl_balances() for acc in pl_accounts: - if flt(acc.balance_in_company_currency): + if flt(acc.bal_in_company_currency): gl_entries.append(self.get_gl_dict({ "account": acc.account, "cost_center": acc.cost_center, "account_currency": acc.account_currency, - "debit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \ - if flt(acc.balance_in_account_currency) < 0 else 0, - "debit": abs(flt(acc.balance_in_company_currency)) \ - if flt(acc.balance_in_company_currency) < 0 else 0, - "credit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \ - if flt(acc.balance_in_account_currency) > 0 else 0, - "credit": abs(flt(acc.balance_in_company_currency)) \ - if flt(acc.balance_in_company_currency) > 0 else 0 + "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, + "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, + "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, + "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0 }, item=acc)) - net_pl_balance += flt(acc.balance_in_company_currency) + net_pl_balance += flt(acc.bal_in_company_currency) if net_pl_balance: - cost_center = frappe.db.get_value("Company", self.company, "cost_center") - gl_entry = self.get_gl_dict({ - "account": self.closing_account_head, - "debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0, - "debit": abs(net_pl_balance) if net_pl_balance > 0 else 0, - "credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0, - "credit": abs(net_pl_balance) if net_pl_balance < 0 else 0, - "cost_center": cost_center - }) - - for dimension in accounting_dimensions: - gl_entry.update({ - dimension: default_dimensions.get(self.company, {}).get(dimension) - }) - - gl_entries.append(gl_entry) + if self.cost_center_wise_pnl: + costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts) + gl_entries += costcenter_wise_gl_entries + else: + gl_entry = self.get_pnl_gl_entry(net_pl_balance) + gl_entries.append(gl_entry) from erpnext.accounts.general_ledger import make_gl_entries make_gl_entries(gl_entries) + + def get_pnl_gl_entry(self, net_pl_balance): + cost_center = frappe.db.get_value("Company", self.company, "cost_center") + gl_entry = self.get_gl_dict({ + "account": self.closing_account_head, + "debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0, + "debit": abs(net_pl_balance) if net_pl_balance > 0 else 0, + "credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0, + "credit": abs(net_pl_balance) if net_pl_balance < 0 else 0, + "cost_center": cost_center + }) + + self.update_default_dimensions(gl_entry) + + return gl_entry + + def get_costcenter_wise_pnl_gl_entries(self, pl_accounts): + company_cost_center = frappe.db.get_value("Company", self.company, "cost_center") + gl_entries = [] + + for acc in pl_accounts: + if flt(acc.bal_in_company_currency): + gl_entry = self.get_gl_dict({ + "account": self.closing_account_head, + "cost_center": acc.cost_center or company_cost_center, + "account_currency": acc.account_currency, + "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, + "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, + "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, + "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0 + }, item=acc) + + self.update_default_dimensions(gl_entry) + + gl_entries.append(gl_entry) + + return gl_entries + + def update_default_dimensions(self, gl_entry): + if not self.accounting_dimensions: + self.accounting_dimensions = get_accounting_dimensions() + + _, default_dimensions = get_dimensions() + for dimension in self.accounting_dimensions: + gl_entry.update({ + dimension: default_dimensions.get(self.company, {}).get(dimension) + }) + + def get_pl_balances(self): + """Get balance for dimension-wise pl accounts""" + + dimension_fields = ['t1.cost_center'] + + self.accounting_dimensions = get_accounting_dimensions() + for dimension in self.accounting_dimensions: + dimension_fields.append('t1.{0}'.format(dimension)) - def get_pl_balances(self, dimension_fields): - """Get balance for pl accounts""" return frappe.db.sql(""" select t1.account, t2.account_currency, {dimension_fields}, - sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as balance_in_account_currency, - sum(t1.debit) - sum(t1.credit) as balance_in_company_currency + sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency, + sum(t1.debit) - sum(t1.credit) as bal_in_company_currency from `tabGL Entry` t1, `tabAccount` t2 where t1.account = t2.name and t2.report_type = 'Profit and Loss' and t2.docstatus < 2 and t2.company = %s diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index eb02d97b78..2f29372b01 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -8,6 +8,7 @@ import frappe from frappe.utils import flt, today from erpnext.accounts.utils import get_fiscal_year, now from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice class TestPeriodClosingVoucher(unittest.TestCase): def test_closing_entry(self): @@ -65,6 +66,58 @@ class TestPeriodClosingVoucher(unittest.TestCase): self.assertEqual(gle_for_random_expense_account[0].amount_in_account_currency, -1*random_expense_account[0].balance_in_account_currency) + def test_cost_center_wise_posting(self): + frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") + + company = create_company() + surplus_account = create_account() + + cost_center1 = create_cost_center("Test Cost Center 1") + cost_center2 = create_cost_center("Test Cost Center 2") + + create_sales_invoice( + company=company, + cost_center=cost_center1, + income_account="Sales - TPC", + expense_account="Cost of Goods Sold - TPC", + rate=400, + debit_to="Debtors - TPC" + ) + create_sales_invoice( + company=company, + cost_center=cost_center2, + income_account="Sales - TPC", + expense_account="Cost of Goods Sold - TPC", + rate=200, + debit_to="Debtors - TPC" + ) + + pcv = frappe.get_doc({ + "transaction_date": today(), + "posting_date": today(), + "fiscal_year": get_fiscal_year(today())[0], + "company": "Test PCV Company", + "cost_center_wise_pnl": 1, + "closing_account_head": surplus_account, + "remarks": "Test", + "doctype": "Period Closing Voucher" + }) + pcv.insert() + pcv.submit() + + expected_gle = ( + ('Sales - TPC', 200.0, 0.0, cost_center2), + (surplus_account, 0.0, 200.0, cost_center2), + ('Sales - TPC', 400.0, 0.0, cost_center1), + (surplus_account, 0.0, 400.0, cost_center1) + ) + + pcv_gle = frappe.db.sql(""" + select account, debit, credit, cost_center from `tabGL Entry` where voucher_no=%s + """, (pcv.name)) + + self.assertTrue(pcv_gle, expected_gle) + def make_period_closing_voucher(self): pcv = frappe.get_doc({ "doctype": "Period Closing Voucher", @@ -80,6 +133,38 @@ class TestPeriodClosingVoucher(unittest.TestCase): return pcv +def create_company(): + company = frappe.get_doc({ + 'doctype': 'Company', + 'company_name': "Test PCV Company", + 'country': 'United States', + 'default_currency': 'USD' + }) + company.insert(ignore_if_duplicate = True) + return company.name + +def create_account(): + account = frappe.get_doc({ + "account_name": "Reserve and Surplus", + "is_group": 0, + "company": "Test PCV Company", + "root_type": "Liability", + "report_type": "Balance Sheet", + "account_currency": "USD", + "parent_account": "Current Liabilities - TPC", + "doctype": "Account" + }).insert(ignore_if_duplicate = True) + return account.name + +def create_cost_center(cc_name): + costcenter = frappe.get_doc({ + "company": "Test PCV Company", + "cost_center_name": cc_name, + "doctype": "Cost Center", + "parent_cost_center": "Test PCV Company - TPC" + }) + costcenter.insert(ignore_if_duplicate = True) + return costcenter.name test_dependencies = ["Customer", "Cost Center"] test_records = frappe.get_test_records("Period Closing Voucher") diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 8c5a34a0d8..6418d73090 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -107,7 +107,7 @@ frappe.ui.form.on('POS Closing Entry', { frm.set_value("taxes", []); for (let row of frm.doc.payment_reconciliation) { - row.expected_amount = 0; + row.expected_amount = row.opening_amount; } for (let row of frm.doc.pos_transactions) { @@ -154,6 +154,9 @@ function add_to_pos_transaction(d, frm) { function refresh_payments(d, frm) { d.payments.forEach(p => { const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); + if (p.account == d.account_for_change_amount) { + p.amount -= flt(d.change_amount); + } if (payment) { payment.expected_amount += flt(p.amount); payment.difference = payment.closing_amount - payment.expected_amount; diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index f55fdab21c..8ec4ef224c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -140,6 +140,7 @@ class POSInvoice(SalesInvoice): return available_stock = get_stock_availability(d.item_code, d.warehouse) + item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) if flt(available_stock) <= 0: frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.') @@ -213,8 +214,9 @@ class POSInvoice(SalesInvoice): for d in self.get("items"): is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item") if not is_stock_item: - frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ") - .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) + if not frappe.db.exists('Product Bundle', d.item_code): + frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.") + .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) def validate_mode_of_payment(self): if len(self.payments) == 0: @@ -455,15 +457,36 @@ class POSInvoice(SalesInvoice): @frappe.whitelist() def get_stock_availability(item_code, warehouse): + if frappe.db.get_value('Item', item_code, 'is_stock_item'): + bin_qty = get_bin_qty(item_code, warehouse) + pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) + return bin_qty - pos_sales_qty + else: + if frappe.db.exists('Product Bundle', item_code): + return get_bundle_availability(item_code, warehouse) + +def get_bundle_availability(bundle_item_code, warehouse): + product_bundle = frappe.get_doc('Product Bundle', bundle_item_code) + + bundle_bin_qty = 1000000 + for item in product_bundle.items: + item_bin_qty = get_bin_qty(item.item_code, warehouse) + item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) + available_qty = item_bin_qty - item_pos_reserved_qty + + max_available_bundles = available_qty / item.qty + if bundle_bin_qty > max_available_bundles: + bundle_bin_qty = max_available_bundles + + pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse) + return bundle_bin_qty - pos_sales_qty + +def get_bin_qty(item_code, warehouse): bin_qty = frappe.db.sql("""select actual_qty from `tabBin` where item_code = %s and warehouse = %s limit 1""", (item_code, warehouse), as_dict=1) - pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) - - bin_qty = bin_qty[0].actual_qty or 0 if bin_qty else 0 - - return bin_qty - pos_sales_qty + return bin_qty[0].actual_qty or 0 if bin_qty else 0 def get_pos_reserved_qty(item_code, warehouse): reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty @@ -522,4 +545,4 @@ def add_return_modes(doc, pos_profile): mode_of_payment = pos_payment_method.mode_of_payment if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]: payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company) - append_payment(payment_mode[0]) \ No newline at end of file + append_payment(payment_mode[0]) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index aedf1c6f1a..556f49d34c 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -152,7 +152,7 @@ class PricingRule(Document): frappe.throw(_("Valid from date must be less than valid upto date")) def validate_condition(self): - if self.condition and ("=" in self.condition) and re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", self.condition): + if self.condition and ("=" in self.condition) and re.match(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+', self.condition): frappe.throw(_("Invalid condition expression")) #-------------------------------------------------------------------------------- diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index d3d3ffa17f..9157821520 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -837,6 +837,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "base_rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment (Company Currency)", @@ -883,6 +884,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment", @@ -1380,7 +1382,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-04-30 22:45:58.334107", + "modified": "2021-06-09 12:30:25.632109", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 66be11ff23..53db689c84 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -636,8 +636,8 @@ class TestPurchaseInvoice(unittest.TestCase): def test_rejected_serial_no(self): pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1, - rejected_qty=1, rate=500, update_stock=1, - rejected_warehouse = "_Test Rejected Warehouse - _TC") + rejected_qty=1, rate=500, update_stock=1, rejected_warehouse = "_Test Rejected Warehouse - _TC", + allow_zero_valuation_rate=1) self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"), pi.get("items")[0].warehouse) @@ -994,7 +994,8 @@ def make_purchase_invoice(**args): "project": args.project, "rejected_warehouse": args.rejected_warehouse or "", "rejected_serial_no": args.rejected_serial_no or "", - "asset_location": args.location or "" + "asset_location": args.location or "", + "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0 }) if args.get_taxes_and_charges: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 1808005f62..f813425e6b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte var me = this; this._super(); - this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet']; + this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry']; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format this.frm.set_df_property("debit_to", "print_hide", 0); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 023f4b049c..0b8d28aef0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -531,7 +531,7 @@ class SalesInvoice(SellingController): # set pos values in items for item in self.get("items"): if item.get('item_code'): - profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos) + profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos, update_data=True) for fname, val in iteritems(profile_details): if (not for_validate) or (for_validate and not item.get(fname)): item.set(fname, val) @@ -849,7 +849,6 @@ class SalesInvoice(SellingController): self.make_loyalty_point_redemption_gle(gl_entries) self.make_pos_gl_entries(gl_entries) - self.make_gle_for_change_amount(gl_entries) self.make_write_off_gl_entry(gl_entries) self.make_gle_for_rounding_adjustment(gl_entries) @@ -983,7 +982,13 @@ class SalesInvoice(SellingController): def make_pos_gl_entries(self, gl_entries): if cint(self.is_pos): + + skip_change_gl_entries = not cint(frappe.db.get_single_value('Accounts Settings', 'post_change_gl_entries')) + for payment_mode in self.payments: + if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: + payment_mode.base_amount -= self.change_amount + if payment_mode.amount: # POS, make payment entries gl_entries.append( @@ -1015,8 +1020,11 @@ class SalesInvoice(SellingController): }, payment_mode_account_currency, item=self) ) + if not skip_change_gl_entries: + self.make_gle_for_change_amount(gl_entries) + def make_gle_for_change_amount(self, gl_entries): - if cint(self.is_pos) and self.change_amount: + if self.change_amount: if self.account_for_change_amount: gl_entries.append( self.get_gl_dict({ diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index b35686f4f0..f493acd609 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -713,7 +713,7 @@ class TestSalesInvoice(unittest.TestCase): si.submit() self.assertEqual(si.paid_amount, 100.0) - self.pos_gl_entry(si, pos, 50) + self.validate_pos_gl_entry(si, pos, 50) def test_pos_returns_with_repayment(self): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return @@ -749,7 +749,7 @@ class TestSalesInvoice(unittest.TestCase): make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") - pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", + make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1") pos = create_sales_invoice(company= "_Test Company with perpetual inventory", @@ -770,7 +770,45 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(pos.grand_total, 100.0) self.assertEqual(pos.write_off_amount, -5) - def pos_gl_entry(self, si, pos, cash_amount): + def test_pos_with_no_gl_entry_for_change_amount(self): + frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0) + + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") + + make_purchase_receipt(company= "_Test Company with perpetual inventory", + item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1") + + pos = create_sales_invoice(company= "_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", + income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", do_not_save=True) + + pos.is_pos = 1 + pos.update_stock = 1 + + taxes = get_taxes_and_charges() + pos.taxes = [] + for tax in taxes: + pos.append("taxes", tax) + + pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60}) + + pos.insert() + pos.submit() + + self.assertEqual(pos.grand_total, 100.0) + self.assertEqual(pos.change_amount, 10) + + self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True) + + frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 1) + + def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False): + if validate_without_change_gle: + cash_amount -= pos.change_amount + # check stock ledger entries sle = frappe.db.sql("""select * from `tabStock Ledger Entry` where voucher_type = 'Sales Invoice' and voucher_no = %s""", diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index f1717c50d8..d4b249429b 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -185,10 +185,10 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): for d in gl_map: if d.account == round_off_account: round_off_gle = d - if d.debit_in_account_currency: - debit_credit_diff -= flt(d.debit_in_account_currency) + if d.debit: + debit_credit_diff -= flt(d.debit) else: - debit_credit_diff += flt(d.credit_in_account_currency) + debit_credit_diff += flt(d.credit) round_off_account_exists = True if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)): diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index db605f7285..a11b77a6f6 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -584,6 +584,7 @@ class ReceivablePayableReport(object): `tabGL Entry` where docstatus < 2 + and is_cancelled = 0 and party_type=%s and (party is not null and party != '') {1} {2} {3}""" diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 30a270c204..3cd4b802c1 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -566,7 +566,7 @@ class TestAsset(unittest.TestCase): doc = make_invoice(pr.name) self.assertEqual('Asset Received But Not Billed - _TC', doc.items[0].expense_account) - + def test_asset_cwip_toggling_cases(self): cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting") name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"]) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index ee2beea67f..8677c71bc5 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -765,6 +765,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "base_rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment (Company Currency)", @@ -810,6 +811,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment", @@ -1124,7 +1126,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-01-20 22:07:23.487138", + "modified": "2021-04-19 00:55:30.781375", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 40fbe2c26e..0a51a8e9a1 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -576,6 +576,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "base_rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment (Company Currency", @@ -620,6 +621,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment", @@ -802,7 +804,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-12-03 15:18:29.073368", + "modified": "2021-04-19 00:58:20.995491", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 6900938236..c1fc6fb82f 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -9,12 +9,12 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import execute import json, frappe, unittest -class TestSubcontractedItemToBeReceived(unittest.TestCase): +class TestSubcontractedItemToBeTransferred(unittest.TestCase): - def test_pending_and_received_qty(self): + def test_pending_and_transferred_qty(self): po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes') - make_stock_entry(item_code='_Test Item', target='_Test Warehouse 1 - _TC', qty=100, basic_rate=100) - make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse 1 - _TC', qty=100, basic_rate=100) + make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100) + make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100) transfer_subcontracted_raw_materials(po.name) col, data = execute(filters=frappe._dict({'supplier': po.supplier, 'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)), @@ -38,7 +38,8 @@ def transfer_subcontracted_raw_materials(po): 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}] rm_item_string = json.dumps(rm_item) se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string)) + se.from_warehouse = '_Test Warehouse 1 - _TC' se.to_warehouse = '_Test Warehouse 1 - _TC' se.stock_entry_type = 'Send to Subcontractor' se.save() - se.submit() \ No newline at end of file + se.submit() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 544e624725..401dfdf0df 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -225,7 +225,7 @@ class AccountsController(TransactionBase): def validate_date_with_fiscal_year(self): if self.meta.get_field("fiscal_year"): - date_field = "" + date_field = None if self.meta.get_field("posting_date"): date_field = "posting_date" elif self.meta.get_field("transaction_date"): @@ -1011,7 +1011,6 @@ class AccountsController(TransactionBase): else: grand_total -= self.get("total_advance") base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total")) - if total != flt(grand_total, self.precision("grand_total")) or \ base_total != flt(base_grand_total, self.precision("base_grand_total")): frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total")) @@ -1450,6 +1449,7 @@ def validate_and_delete_children(parent, data): for d in deleted_children: update_bin_on_delete(d, parent.doctype) + @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def check_doc_permissions(doc, perm_type='create'): diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 46301b7587..81ac234e70 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe import erpnext +import json from frappe.desk.reportview import get_match_cond, get_filters_cond from frappe.utils import nowdate, getdate from collections import defaultdict @@ -198,13 +199,16 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): conditions = [] + if isinstance(filters, str): + filters = json.loads(filters) + #Get searchfields from meta and use in Item Link field query meta = frappe.get_meta("Item", cached=True) searchfields = meta.get_search_fields() if "description" in searchfields: searchfields.remove("description") - + columns = '' extra_searchfields = [field for field in searchfields if not field in ["name", "item_group", "description"]] @@ -216,9 +220,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if not field in searchfields] searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) - if filters.get('supplier'): - item_group_list = frappe.get_all('Supplier Item Group', filters = {'supplier': filters.get('supplier')}, fields = ['item_group']) - + if filters and isinstance(filters, dict) and filters.get('supplier'): + item_group_list = frappe.get_all('Supplier Item Group', + filters = {'supplier': filters.get('supplier')}, fields = ['item_group']) + item_groups = [] for i in item_group_list: item_groups.append(i.item_group) @@ -227,7 +232,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if item_groups: filters['item_group'] = ['in', item_groups] - + description_cond = '' if frappe.db.count('Item', cache=True) < 50000: # scan description only if items are less than 50000 diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 41ca404d9b..0da723d56e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1,17 +1,21 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals -import frappe, erpnext -from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate -from frappe import _ -import frappe.defaults +import json from collections import defaultdict -from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced + +import frappe +import frappe.defaults +from frappe import _ +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate + +import erpnext from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map +from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController -from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock import get_warehouse_account_map +from erpnext.stock.stock_ledger import get_valuation_rate + class QualityInspectionRequiredError(frappe.ValidationError): pass class QualityInspectionRejectedError(frappe.ValidationError): pass @@ -189,7 +193,6 @@ class StockController(AccountsController): if hasattr(self, "items"): item_doclist = self.get("items") elif self.doctype == "Stock Reconciliation": - import json item_doclist = [] data = json.loads(self.reconciliation_json) for row in data[data.index(self.head_row)+1:]: @@ -319,7 +322,7 @@ class StockController(AccountsController): return serialized_items def validate_warehouse(self): - from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse + from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company warehouses = list(set([d.warehouse for d in self.get("items") if getattr(d, "warehouse", None)])) @@ -498,6 +501,39 @@ class StockController(AccountsController): check_if_stock_and_account_balance_synced(self.posting_date, self.company, self.doctype, self.name) + +@frappe.whitelist() +def make_quality_inspections(doctype, docname, items): + if isinstance(items, str): + items = json.loads(items) + + inspections = [] + for item in items: + if flt(item.get("sample_size")) > flt(item.get("qty")): + frappe.throw(_("{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})").format( + item_name=item.get("item_name"), + sample_size=item.get("sample_size"), + accepted_quantity=item.get("qty") + )) + + quality_inspection = frappe.get_doc({ + "doctype": "Quality Inspection", + "inspection_type": "Incoming", + "inspected_by": frappe.session.user, + "reference_type": doctype, + "reference_name": docname, + "item_code": item.get("item_code"), + "description": item.get("description"), + "sample_size": flt(item.get("sample_size")), + "item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None, + "batch_no": item.get("batch_no") + }).insert() + quality_inspection.save() + inspections.append(quality_inspection.name) + + return inspections + + def is_reposting_pending(): return frappe.db.exists("Repost Item Valuation", {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 2e09a76c0f..4ba4140244 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -280,7 +280,6 @@ "read_only": 1 }, { - "depends_on": "eval:", "fieldname": "territory", "fieldtype": "Link", "label": "Territory", @@ -431,7 +430,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2021-01-06 19:42:46.190051", + "modified": "2021-06-04 10:11:22.831139", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/education/doctype/fees/test_fees.py b/erpnext/education/doctype/fees/test_fees.py index eedc2ae730..c6bb704b41 100644 --- a/erpnext/education/doctype/fees/test_fees.py +++ b/erpnext/education/doctype/fees/test_fees.py @@ -9,8 +9,7 @@ from frappe.utils import nowdate from frappe.utils.make_random import get_random from erpnext.education.doctype.program.test_program import make_program_and_linked_courses -# test_records = frappe.get_test_records('Fees') - +test_dependencies = ['Company'] class TestFees(unittest.TestCase): def test_fees(self): diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py index 8f51fef847..9db8a4a90d 100644 --- a/erpnext/education/utils.py +++ b/erpnext/education/utils.py @@ -219,7 +219,6 @@ def get_quiz(quiz_name, course): try: quiz = frappe.get_doc("Quiz", quiz_name) questions = quiz.get_questions() - duration = quiz.duration except: frappe.throw(_("Quiz {0} does not exist").format(quiz_name), frappe.DoesNotExistError) return None @@ -236,15 +235,17 @@ def get_quiz(quiz_name, course): return { 'questions': questions, 'activity': None, - 'duration':duration + 'is_time_bound': quiz.is_time_bound, + 'duration': quiz.duration } student = get_current_student() course_enrollment = get_enrollment("course", course, student.name) status, score, result, time_taken = check_quiz_completion(quiz, course_enrollment) return { - 'questions': questions, + 'questions': questions, 'activity': {'is_complete': status, 'score': score, 'result': result, 'time_taken': time_taken}, + 'is_time_bound': quiz.is_time_bound, 'duration': quiz.duration } @@ -372,9 +373,9 @@ def check_content_completion(content_name, content_type, enrollment_name): def check_quiz_completion(quiz, enrollment_name): attempts = frappe.get_all("Quiz Activity", filters={ - 'enrollment': enrollment_name, + 'enrollment': enrollment_name, 'quiz': quiz.name - }, + }, fields=["name", "activity_date", "score", "status", "time_taken"] ) status = False if quiz.max_attempts == 0 else bool(len(attempts) >= quiz.max_attempts) @@ -389,4 +390,4 @@ def check_quiz_completion(quiz, enrollment_name): time_taken = attempts[0]['time_taken'] if result == 'Pass': status = True - return status, score, result, time_taken \ No newline at end of file + return status, score, result, time_taken diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index d370fbcda7..3c2e59ab82 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -81,7 +81,7 @@ class TestMpesaSettings(unittest.TestCase): integration_request.reload() self.assertEqual(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R") self.assertEqual(integration_request.status, "Completed") - + frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") integration_request.delete() pr.reload() @@ -139,7 +139,7 @@ class TestMpesaSettings(unittest.TestCase): pr.cancel() pr.delete() pos_invoice.delete() - + def test_processing_of_only_one_succes_callback_payload(self): create_mpesa_settings(payment_gateway_name="Payment") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 5f990cdd03..42d4b9b2b4 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -99,5 +99,7 @@ class PlaidConnector(): response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) transactions.extend(response["transactions"]) return transactions + except ItemError as e: + raise e except Exception: frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index bbc2ca8846..37bf282450 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -16,6 +16,10 @@ frappe.ui.form.on('Plaid Settings', { new erpnext.integrations.plaidLink(frm); }); + frm.add_custom_button(__('Reset Plaid Link'), () => { + new erpnext.integrations.plaidLink(frm); + }); + frm.add_custom_button(__("Sync Now"), () => { frappe.call({ method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization", diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index ce15e47c5e..3ef069b5e2 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -12,6 +12,7 @@ from frappe.desk.doctype.tag.tag import add_tag from frappe.model.document import Document from frappe.utils import add_months, formatdate, getdate, today +from plaid.errors import ItemError class PlaidSettings(Document): @staticmethod @@ -51,7 +52,7 @@ def add_institution(token, response): }) bank.insert() except Exception: - frappe.throw(frappe.get_traceback()) + frappe.log_error(frappe.get_traceback(), title=_('Plaid Link Error')) else: bank = frappe.get_doc("Bank", response["institution"]["name"]) bank.plaid_access_token = access_token @@ -83,7 +84,12 @@ def add_bank_accounts(response, bank, company): if not acc_subtype: add_account_subtype(account["subtype"]) - if not frappe.db.exists("Bank Account", dict(integration_id=account["id"])): + existing_bank_account = frappe.db.exists("Bank Account", { + 'account_name': account["name"], + 'bank': bank["bank_name"] + }) + + if not existing_bank_account: try: new_account = frappe.get_doc({ "doctype": "Bank Account", @@ -103,10 +109,27 @@ def add_bank_accounts(response, bank, company): except frappe.UniqueValidationError: frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"])) except Exception: - frappe.throw(frappe.get_traceback()) + frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error")) + frappe.throw(_("There was an error creating Bank Account while linking with Plaid."), + title=_("Plaid Link Failed")) else: - result.append(frappe.db.get_value("Bank Account", dict(integration_id=account["id"]), "name")) + try: + existing_account = frappe.get_doc('Bank Account', existing_bank_account) + existing_account.update({ + "bank": bank["bank_name"], + "account_name": account["name"], + "account_type": account.get("type", ""), + "account_subtype": account.get("subtype", ""), + "mask": account.get("mask", ""), + "integration_id": account["id"] + }) + existing_account.save() + result.append(existing_bank_account) + except Exception: + frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error")) + frappe.throw(_("There was an error updating Bank Account {} while linking with Plaid.").format( + existing_bank_account), title=_("Plaid Link Failed")) return result @@ -172,9 +195,16 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): account_id = None plaid = PlaidConnector(access_token) - transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id) - return transactions + try: + transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id) + except ItemError as e: + if e.code == "ITEM_LOGIN_REQUIRED": + msg = _("There was an error syncing transactions.") + " " + msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " " + frappe.log_error(msg, title=_("Plaid Link Refresh Required")) + + return transactions or [] def new_bank_transaction(transaction): diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py index d079bedb42..113fa513f9 100644 --- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py @@ -29,7 +29,7 @@ class TestTherapyPlan(unittest.TestCase): self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') patient, medical_department, practitioner = create_healthcare_docs() - appointment = create_appointment(patient, practitioner, nowdate()) + appointment = create_appointment(patient, practitioner, nowdate()) session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name) session = frappe.get_doc(session) session.submit() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 55169dffba..8ad77a1524 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -332,7 +332,9 @@ scheduler_events = { "erpnext.projects.doctype.project.project.collect_project_status", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", - "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", + "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders" + ], + "hourly_long": [ "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" ], "daily": [ diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 0bf551e178..cee6f374fd 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -4,8 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \ - comma_or, get_fullname, add_days, nowdate, get_datetime_str +from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee @@ -85,7 +84,7 @@ class LeaveApplication(Document): def validate_dates(self): if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"): - if self.from_date and self.from_date < frappe.utils.today(): + if self.from_date and getdate(self.from_date) < getdate(): allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application") if allowed_role not in frappe.get_roles(): frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role)) @@ -248,9 +247,9 @@ class LeaveApplication(Document): self.throw_overlap_error(d) def throw_overlap_error(self, d): - msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee, - d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \ - + """ {0}""".format(d["name"]) + form_link = get_link_to_form("Leave Application", d.name) + msg = _("Employee {0} has already applied for {1} between {2} and {3} : {4}").format(self.employee, + d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date']), form_link) frappe.throw(msg, OverlapError) def get_total_leaves_on_half_day(self): @@ -356,7 +355,7 @@ class LeaveApplication(Document): sender = dict() sender['email'] = frappe.get_doc('User', frappe.session.user).email - sender['full_name'] = frappe.utils.get_fullname(sender['email']) + sender['full_name'] = get_fullname(sender['email']) try: frappe.sendmail( @@ -823,4 +822,4 @@ def get_leave_approver(employee): leave_approver = frappe.db.get_value('Department Approver', {'parent': department, 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') - return leave_approver \ No newline at end of file + return leave_approver diff --git a/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py index 6e151d0e3c..03b0cf3da2 100644 --- a/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py @@ -5,11 +5,18 @@ from __future__ import unicode_literals import frappe import unittest +import erpnext from frappe.utils import getdate from erpnext.hr.doctype.upload_attendance.upload_attendance import get_data from erpnext.hr.doctype.employee.test_employee import make_employee +test_dependencies = ['Holiday List'] + class TestUploadAttendance(unittest.TestCase): + @classmethod + def setUpClass(cls): + frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List') + def test_date_range(self): employee = make_employee("test_employee@company.com") employee_doc = frappe.get_doc("Employee", employee) diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py index cf0048c1a7..ed52c4e122 100644 --- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import nowdate,flt, cstr,random_string +from frappe.utils import nowdate, flt, cstr, random_string from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim @@ -18,23 +18,13 @@ class TestVehicleLog(unittest.TestCase): self.employee_id = make_employee("testdriver@example.com", company="_Test Company") self.license_plate = get_vehicle(self.employee_id) - + def tearDown(self): frappe.delete_doc("Vehicle", self.license_plate, force=1) frappe.delete_doc("Employee", self.employee_id, force=1) def test_make_vehicle_log_and_syncing_of_odometer_value(self): - vehicle_log = frappe.get_doc({ - "doctype": "Vehicle Log", - "license_plate": cstr(self.license_plate), - "employee": self.employee_id, - "date":frappe.utils.nowdate(), - "odometer":5010, - "fuel_qty":frappe.utils.flt(50), - "price": frappe.utils.flt(500) - }) - vehicle_log.save() - vehicle_log.submit() + vehicle_log = make_vehicle_log(self.license_plate, self.employee_id) #checking value of vehicle odometer value on submit. vehicle = frappe.get_doc("Vehicle", self.license_plate) @@ -51,19 +41,9 @@ class TestVehicleLog(unittest.TestCase): self.assertEqual(vehicle.last_odometer, current_odometer - distance_travelled) vehicle_log.delete() - + def test_vehicle_log_fuel_expense(self): - vehicle_log = frappe.get_doc({ - "doctype": "Vehicle Log", - "license_plate": cstr(self.license_plate), - "employee": self.employee_id, - "date": frappe.utils.nowdate(), - "odometer":5010, - "fuel_qty":frappe.utils.flt(50), - "price": frappe.utils.flt(500) - }) - vehicle_log.save() - vehicle_log.submit() + vehicle_log = make_vehicle_log(self.license_plate, self.employee_id) expense_claim = make_expense_claim(vehicle_log.name) fuel_expense = expense_claim.expenses[0].amount @@ -73,6 +53,18 @@ class TestVehicleLog(unittest.TestCase): frappe.delete_doc("Expense Claim", expense_claim.name) frappe.delete_doc("Vehicle Log", vehicle_log.name) + def test_vehicle_log_with_service_expenses(self): + vehicle_log = make_vehicle_log(self.license_plate, self.employee_id, with_services=True) + + expense_claim = make_expense_claim(vehicle_log.name) + expenses = expense_claim.expenses[0].amount + self.assertEqual(expenses, 27000) + + vehicle_log.cancel() + frappe.delete_doc("Expense Claim", expense_claim.name) + frappe.delete_doc("Vehicle Log", vehicle_log.name) + + def get_vehicle(employee_id): license_plate=random_string(10).upper() vehicle = frappe.get_doc({ @@ -81,15 +73,46 @@ def get_vehicle(employee_id): "make": "Maruti", "model": "PCM", "employee": employee_id, - "last_odometer":5000, - "acquisition_date":frappe.utils.nowdate(), + "last_odometer": 5000, + "acquisition_date": nowdate(), "location": "Mumbai", "chassis_no": "1234ABCD", "uom": "Litre", - "vehicle_value":frappe.utils.flt(500000) + "vehicle_value": flt(500000) }) try: vehicle.insert() except frappe.DuplicateEntryError: pass - return license_plate \ No newline at end of file + return license_plate + + +def make_vehicle_log(license_plate, employee_id, with_services=False): + vehicle_log = frappe.get_doc({ + "doctype": "Vehicle Log", + "license_plate": cstr(license_plate), + "employee": employee_id, + "date": nowdate(), + "odometer": 5010, + "fuel_qty": flt(50), + "price": flt(500) + }) + + if with_services: + vehicle_log.append("service_detail", { + "service_item": "Oil Change", + "type": "Inspection", + "frequency": "Mileage", + "expense_amount": flt(500) + }) + vehicle_log.append("service_detail", { + "service_item": "Wheels", + "type": "Change", + "frequency": "Half Yearly", + "expense_amount": flt(1500) + }) + + vehicle_log.save() + vehicle_log.submit() + + return vehicle_log \ No newline at end of file diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.json b/erpnext/hr/doctype/vehicle_log/vehicle_log.json index 619e295ebe..4ea904542d 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.json +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2016-09-03 14:14:51.788550", "doctype": "DocType", @@ -10,7 +11,6 @@ "naming_series", "license_plate", "employee", - "column_break_4", "column_break_7", "model", "make", @@ -65,10 +65,6 @@ "options": "Employee", "reqd": 1 }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, { "fieldname": "column_break_7", "fieldtype": "Column Break" @@ -142,7 +138,6 @@ { "fieldname": "service_detail", "fieldtype": "Table", - "label": "Service Detail", "options": "Vehicle Service" }, { @@ -158,7 +153,7 @@ "fetch_from": "license_plate.last_odometer", "fieldname": "last_odometer", "fieldtype": "Int", - "label": "last Odometer Value ", + "label": "Last Odometer Value ", "read_only": 1, "reqd": 1 }, @@ -168,7 +163,8 @@ } ], "is_submittable": 1, - "modified": "2020-03-18 16:45:45.060761", + "links": [], + "modified": "2021-05-17 00:10:21.188352", "modified_by": "Administrator", "module": "HR", "name": "Vehicle Log", diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js index 05728a297b..8bb3457190 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js @@ -37,5 +37,22 @@ frappe.query_reports["Employee Leave Balance"] = { "fieldtype": "Link", "options": "Employee", } - ] + ], + + onload: () => { + frappe.call({ + type: "GET", + method: "erpnext.hr.utils.get_leave_period", + args: { + "from_date": frappe.defaults.get_default("year_start_date"), + "to_date": frappe.defaults.get_default("year_end_date"), + "company": frappe.defaults.get_user_default("Company") + }, + freeze: true, + callback: (data) => { + frappe.query_report.set_filter_value("from_date", data.message[0].from_date); + frappe.query_report.set_filter_value("to_date", data.message[0].to_date); + } + }); + } } diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 06f9160363..4dd4570e8c 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -6,15 +6,16 @@ import frappe from frappe.utils import flt, add_days from frappe import _ from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on +from itertools import groupby def execute(filters=None): if filters.to_date <= filters.from_date: - frappe.throw(_('"From date" can not be greater than or equal to "To date"')) + frappe.throw(_('"From Date" can not be greater than or equal to "To Date"')) columns = get_columns() data = get_data(filters) - - return columns, data + charts = get_chart_data(data) + return columns, data, None, charts def get_columns(): columns = [{ @@ -31,9 +32,10 @@ def get_columns(): 'options': 'Employee' }, { 'label': _('Employee Name'), - 'fieldtype': 'Data', + 'fieldtype': 'Dynamic Link', 'fieldname': 'employee_name', 'width': 100, + 'options': 'employee' }, { 'label': _('Opening Balance'), 'fieldtype': 'float', @@ -64,8 +66,7 @@ def get_columns(): return columns def get_data(filters): - leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC") - + leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name') conditions = get_conditions(filters) user = frappe.session.user @@ -113,12 +114,8 @@ def get_data(filters): # not be shown on the basis of days left it create in user mind for carry_forward leave row.closing_balance = (new_allocation + opening - (row.leaves_expired + leaves_taken)) - - row.indent = 1 data.append(row) - new_leaves_allocated = 0 - return data @@ -129,27 +126,37 @@ def get_conditions(filters): if filters.get('employee'): conditions['name'] = filters.get('employee') - if filters.get('employee'): - conditions['name'] = filters.get('employee') - if filters.get('company'): conditions['company'] = filters.get('company') + if filters.get('department'): + conditions['department'] = filters.get('department') + return conditions def get_department_leave_approver_map(department=None): - conditions='' - if department: - conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department} # get current department and all its child - department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec - + department_list = frappe.get_list('Department', + filters={ + 'disabled': 0 + }, + or_filters={ + 'name': department, + 'parent_department': department + }, + fields=['name'], + pluck='name' + ) # retrieve approvers list from current department and from its subsequent child departments - approver_list = frappe.get_all('Department Approver', filters={ - 'parentfield': 'leave_approvers', - 'parent': ('in', department_list) - }, fields=['parent', 'approver'], as_list=1) + approver_list = frappe.get_all('Department Approver', + filters={ + 'parentfield': 'leave_approvers', + 'parent': ('in', department_list) + }, + fields=['parent', 'approver'], + as_list=1 + ) approvers = {} @@ -190,3 +197,40 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): new_allocation += record.leaves return new_allocation, expired_leaves + +def get_chart_data(data): + labels = [] + datasets = [] + employee_data = data + + if data and data[0].get('employee_name'): + get_dataset_for_chart(employee_data, datasets, labels) + + chart = { + 'data': { + 'labels': labels, + 'datasets': datasets + }, + 'type': 'bar', + 'colors': ['#456789', '#EE8888', '#7E77BF'] + } + + return chart + +def get_dataset_for_chart(employee_data, datasets, labels): + leaves = [] + employee_data = sorted(employee_data, key=lambda k: k['employee_name']) + + for key, group in groupby(employee_data, lambda x: x['employee_name']): + for grp in group: + if grp.closing_balance: + leaves.append(frappe._dict({ + 'leave_type': grp.leave_type, + 'closing_balance': grp.closing_balance + })) + + if leaves: + labels.append(key) + + for leave in leaves: + datasets.append({'name': leave.leave_type, 'values': [leave.closing_balance]}) diff --git a/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py new file mode 100644 index 0000000000..26e0f26392 --- /dev/null +++ b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py @@ -0,0 +1,73 @@ +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import unittest +import frappe +from frappe.utils import getdate +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim +from erpnext.hr.doctype.vehicle_log.test_vehicle_log import get_vehicle, make_vehicle_log +from erpnext.hr.report.vehicle_expenses.vehicle_expenses import execute +from erpnext.accounts.utils import get_fiscal_year + +class TestVehicleExpenses(unittest.TestCase): + @classmethod + def setUpClass(self): + frappe.db.sql('delete from `tabVehicle Log`') + + employee_id = frappe.db.sql('''select name from `tabEmployee` where name="testdriver@example.com"''') + self.employee_id = employee_id[0][0] if employee_id else None + if not self.employee_id: + self.employee_id = make_employee('testdriver@example.com', company='_Test Company') + + self.license_plate = get_vehicle(self.employee_id) + + def test_vehicle_expenses_based_on_fiscal_year(self): + vehicle_log = make_vehicle_log(self.license_plate, self.employee_id, with_services=True) + expense_claim = make_expense_claim(vehicle_log.name) + + # Based on Fiscal Year + filters = { + 'filter_based_on': 'Fiscal Year', + 'fiscal_year': get_fiscal_year(getdate())[0] + } + + report = execute(filters) + + expected_data = [{ + 'vehicle': self.license_plate, + 'make': 'Maruti', + 'model': 'PCM', + 'location': 'Mumbai', + 'log_name': vehicle_log.name, + 'odometer': 5010, + 'date': getdate(), + 'fuel_qty': 50.0, + 'fuel_price': 500.0, + 'fuel_expense': 25000.0, + 'service_expense': 2000.0, + 'employee': self.employee_id + }] + + self.assertEqual(report[1], expected_data) + + # Based on Date Range + fiscal_year = get_fiscal_year(getdate(), as_dict=True) + filters = { + 'filter_based_on': 'Date Range', + 'from_date': fiscal_year.year_start_date, + 'to_date': fiscal_year.year_end_date + } + + report = execute(filters) + self.assertEqual(report[1], expected_data) + + # clean up + vehicle_log.cancel() + frappe.delete_doc('Expense Claim', expense_claim.name) + frappe.delete_doc('Vehicle Log', vehicle_log.name) + + def tearDown(self): + frappe.delete_doc('Vehicle', self.license_plate, force=1) + frappe.delete_doc('Employee', self.employee_id, force=1) diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js index b66bebbec1..879acd18ef 100644 --- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js +++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js @@ -1,31 +1,52 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.require("assets/erpnext/js/financial_statements.js", function() { - frappe.query_reports["Vehicle Expenses"] = { - "filters": [ - { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), - "reqd": 1, - "on_change": function(query_report) { - var fiscal_year = query_report.get_values().fiscal_year; - if (!fiscal_year) { - return; - } - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { - var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); - - frappe.query_report.set_filter({ - from_date: fy.year_start_date, - to_date: fy.year_end_date - }); - }); - } - } - ] - } -}); +frappe.query_reports["Vehicle Expenses"] = { + "filters": [ + { + "fieldname": "filter_based_on", + "label": __("Filter Based On"), + "fieldtype": "Select", + "options": ["Fiscal Year", "Date Range"], + "default": ["Fiscal Year"], + "reqd": 1 + }, + { + "fieldname": "fiscal_year", + "label": __("Fiscal Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", + "reqd": 1 + }, + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "reqd": 1, + "depends_on": "eval: doc.filter_based_on == 'Date Range'", + "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12) + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "reqd": 1, + "depends_on": "eval: doc.filter_based_on == 'Date Range'", + "default": frappe.datetime.nowdate() + }, + { + "fieldname": "vehicle", + "label": __("Vehicle"), + "fieldtype": "Link", + "options": "Vehicle" + }, + { + "fieldname": "employee", + "label": __("Employee"), + "fieldtype": "Link", + "options": "Employee" + } + ] +}; diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.json b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.json index 2ab0c143b8..1a3e5a93bb 100644 --- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.json +++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.json @@ -1,20 +1,23 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2016-09-09 03:33:40.605734", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-02-24 19:59:18.641284", - "modified_by": "Administrator", - "module": "HR", - "name": "Vehicle Expenses", - "owner": "Administrator", - "ref_doctype": "Vehicle", - "report_name": "Vehicle Expenses", - "report_type": "Script Report", + "add_total_row": 1, + "columns": [], + "creation": "2016-09-09 03:33:40.605734", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 2, + "is_standard": "Yes", + "modified": "2021-05-16 22:48:22.767535", + "modified_by": "Administrator", + "module": "HR", + "name": "Vehicle Expenses", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Vehicle", + "report_name": "Vehicle Expenses", + "report_type": "Script Report", "roles": [ { "role": "Fleet Manager" diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py index eab58ffbbc..d847cbb5c9 100644 --- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py +++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py @@ -5,86 +5,209 @@ from __future__ import unicode_literals import frappe import erpnext from frappe import _ -from frappe.utils import flt,cstr +from frappe.utils import flt from erpnext.accounts.report.financial_statements import get_period_list def execute(filters=None): - columns, data, chart = [], [], [] - if filters.get('fiscal_year'): - company = erpnext.get_default_company() - period_list = get_period_list(filters.get('fiscal_year'), filters.get('fiscal_year'), - '', '', 'Fiscal Year', 'Monthly', company=company) - columns=get_columns() - data=get_log_data(filters) - chart=get_chart_data(data,period_list) + filters = frappe._dict(filters or {}) + + columns = get_columns() + data = get_vehicle_log_data(filters) + chart = get_chart_data(data, filters) + return columns, data, None, chart def get_columns(): - columns = [_("License") + ":Link/Vehicle:100", _('Create') + ":data:50", - _("Model") + ":data:50", _("Location") + ":data:100", - _("Log") + ":Link/Vehicle Log:100", _("Odometer") + ":Int:80", - _("Date") + ":Date:100", _("Fuel Qty") + ":Float:80", - _("Fuel Price") + ":Float:100",_("Fuel Expense") + ":Float:100", - _("Service Expense") + ":Float:100" + return [ + { + 'fieldname': 'vehicle', + 'fieldtype': 'Link', + 'label': _('Vehicle'), + 'options': 'Vehicle', + 'width': 150 + }, + { + 'fieldname': 'make', + 'fieldtype': 'Data', + 'label': _('Make'), + 'width': 100 + }, + { + 'fieldname': 'model', + 'fieldtype': 'Data', + 'label': _('Model'), + 'width': 80 + }, + { + 'fieldname': 'location', + 'fieldtype': 'Data', + 'label': _('Location'), + 'width': 100 + }, + { + 'fieldname': 'log_name', + 'fieldtype': 'Link', + 'label': _('Vehicle Log'), + 'options': 'Vehicle Log', + 'width': 100 + }, + { + 'fieldname': 'odometer', + 'fieldtype': 'Int', + 'label': _('Odometer Value'), + 'width': 120 + }, + { + 'fieldname': 'date', + 'fieldtype': 'Date', + 'label': _('Date'), + 'width': 100 + }, + { + 'fieldname': 'fuel_qty', + 'fieldtype': 'Float', + 'label': _('Fuel Qty'), + 'width': 80 + }, + { + 'fieldname': 'fuel_price', + 'fieldtype': 'Float', + 'label': _('Fuel Price'), + 'width': 100 + }, + { + 'fieldname': 'fuel_expense', + 'fieldtype': 'Currency', + 'label': _('Fuel Expense'), + 'width': 150 + }, + { + 'fieldname': 'service_expense', + 'fieldtype': 'Currency', + 'label': _('Service Expense'), + 'width': 150 + }, + { + 'fieldname': 'employee', + 'fieldtype': 'Link', + 'label': _('Employee'), + 'options': 'Employee', + 'width': 150 + } ] + return columns -def get_log_data(filters): - fy = frappe.db.get_value('Fiscal Year', filters.get('fiscal_year'), ['year_start_date', 'year_end_date'], as_dict=True) - data = frappe.db.sql("""select - vhcl.license_plate as "License", vhcl.make as "Make", vhcl.model as "Model", - vhcl.location as "Location", log.name as "Log", log.odometer as "Odometer", - log.date as "Date", log.fuel_qty as "Fuel Qty", log.price as "Fuel Price", - log.fuel_qty * log.price as "Fuel Expense" - from + +def get_vehicle_log_data(filters): + start_date, end_date = get_period_dates(filters) + conditions, values = get_conditions(filters) + + data = frappe.db.sql(""" + SELECT + vhcl.license_plate as vehicle, vhcl.make, vhcl.model, + vhcl.location, log.name as log_name, log.odometer, + log.date, log.employee, log.fuel_qty, + log.price as fuel_price, + log.fuel_qty * log.price as fuel_expense + FROM `tabVehicle` vhcl,`tabVehicle Log` log - where - vhcl.license_plate = log.license_plate and log.docstatus = 1 and date between %s and %s - order by date""" ,(fy.year_start_date, fy.year_end_date), as_dict=1) - dl=list(data) - for row in dl: - row["Service Expense"]= get_service_expense(row["Log"]) - return dl + WHERE + vhcl.license_plate = log.license_plate + and log.docstatus = 1 + and date between %(start_date)s and %(end_date)s + {0} + ORDER BY date""".format(conditions), values, as_dict=1) + + for row in data: + row['service_expense'] = get_service_expense(row.log_name) + + return data + + +def get_conditions(filters): + conditions = '' + + start_date, end_date = get_period_dates(filters) + values = { + 'start_date': start_date, + 'end_date': end_date + } + + if filters.employee: + conditions += ' and log.employee = %(employee)s' + values['employee'] = filters.employee + + if filters.vehicle: + conditions += ' and vhcl.license_plate = %(vehicle)s' + values['vehicle'] = filters.vehicle + + return conditions, values + + +def get_period_dates(filters): + if filters.filter_based_on == 'Fiscal Year' and filters.fiscal_year: + fy = frappe.db.get_value('Fiscal Year', filters.fiscal_year, + ['year_start_date', 'year_end_date'], as_dict=True) + return fy.year_start_date, fy.year_end_date + else: + return filters.from_date, filters.to_date + def get_service_expense(logname): - expense_amount = frappe.db.sql("""select sum(expense_amount) - from `tabVehicle Log` log,`tabVehicle Service` ser - where ser.parent=log.name and log.name=%s""",logname) - return flt(expense_amount[0][0]) if expense_amount else 0 + expense_amount = frappe.db.sql(""" + SELECT sum(expense_amount) + FROM + `tabVehicle Log` log, `tabVehicle Service` service + WHERE + service.parent=log.name and log.name=%s + """, logname) + + return flt(expense_amount[0][0]) if expense_amount else 0.0 + + +def get_chart_data(data, filters): + period_list = get_period_list(filters.fiscal_year, filters.fiscal_year, + filters.from_date, filters.to_date, filters.filter_based_on, 'Monthly') + + fuel_data, service_data = [], [] -def get_chart_data(data,period_list): - fuel_exp_data,service_exp_data,fueldata,servicedata = [],[],[],[] - service_exp_data = [] - fueldata = [] for period in period_list: - total_fuel_exp=0 - total_ser_exp=0 - for row in data: - if row["Date"] <= period.to_date and row["Date"] >= period.from_date: - total_fuel_exp+=flt(row["Fuel Expense"]) - total_ser_exp+=flt(row["Service Expense"]) - fueldata.append([period.key,total_fuel_exp]) - servicedata.append([period.key,total_ser_exp]) + total_fuel_exp = 0 + total_service_exp = 0 + + for row in data: + if row.date <= period.to_date and row.date >= period.from_date: + total_fuel_exp += flt(row.fuel_expense) + total_service_exp += flt(row.service_expense) + + fuel_data.append([period.key, total_fuel_exp]) + service_data.append([period.key, total_service_exp]) + + labels = [period.label for period in period_list] + fuel_exp_data= [row[1] for row in fuel_data] + service_exp_data= [row[1] for row in service_data] - labels = [period.key for period in period_list] - fuel_exp_data= [row[1] for row in fueldata] - service_exp_data= [row[1] for row in servicedata] datasets = [] if fuel_exp_data: datasets.append({ - 'name': 'Fuel Expenses', + 'name': _('Fuel Expenses'), 'values': fuel_exp_data }) + if service_exp_data: datasets.append({ - 'name': 'Service Expenses', + 'name': _('Service Expenses'), 'values': service_exp_data }) + chart = { - "data": { + 'data': { 'labels': labels, 'datasets': datasets - } + }, + 'type': 'line', + 'fieldtype': 'Currency' } - chart["type"] = "line" + return chart diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 80189e87b7..ebb1734347 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -269,6 +269,7 @@ def get_total_exemption_amount(declarations): total_exemption_amount = sum([flt(d.total_exemption_amount) for d in exemptions.values()]) return total_exemption_amount +@frappe.whitelist() def get_leave_period(from_date, to_date, company): leave_period = frappe.db.sql(""" select name, from_date, to_date diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json index 18559dceef..d0b67f7c64 100644 --- a/erpnext/loan_management/workspace/loan_management/loan_management.json +++ b/erpnext/loan_management/workspace/loan_management/loan_management.json @@ -12,7 +12,7 @@ "idx": 0, "is_default": 0, "is_standard": 1, - "label": "Loan Management", + "label": "Loans", "links": [ { "hidden": 0, @@ -220,10 +220,10 @@ "type": "Link" } ], - "modified": "2021-02-18 17:31:53.586508", + "modified": "2021-05-25 17:31:53.586508", "modified_by": "Administrator", "module": "Loan Management", - "name": "Loan Management", + "name": "Loans", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js index ddbcdfde57..44712d543b 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js @@ -2,40 +2,36 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.maintenance"); - frappe.ui.form.on('Maintenance Schedule', { - setup: function(frm) { + 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); - - frm.add_fetch('item_code', 'item_name', 'item_name'); - frm.add_fetch('item_code', 'description', 'description'); }, - onload: function(frm) { + onload: function (frm) { if (!frm.doc.status) { - frm.set_value({status:'Draft'}); + frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { - frm.set_value({transaction_date: frappe.datetime.get_today()}); + frm.set_value({ transaction_date: frappe.datetime.get_today() }); } }, - refresh: function(frm) { + refresh: function (frm) { setTimeout(() => { frm.toggle_display('generate_schedule', !(frm.is_new())); frm.toggle_display('schedule', !(frm.is_new())); - },10); + }, 10); }, - customer: function(frm) { + customer: function (frm) { erpnext.utils.get_party_details(frm) }, - customer_address: function(frm) { + customer_address: function (frm) { erpnext.utils.get_address_display(frm, 'customer_address', 'address_display'); }, - contact_person: function(frm) { + contact_person: function (frm) { erpnext.utils.get_contact_details(frm); }, - generate_schedule: function(frm) { + generate_schedule: function (frm) { if (frm.is_new()) { frappe.msgprint(__('Please save first')); } else { @@ -46,14 +42,14 @@ frappe.ui.form.on('Maintenance Schedule', { // TODO commonify this code erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({ - refresh: function() { - frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} + refresh: function () { + frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer' }; var me = this; if (this.frm.doc.docstatus === 0) { this.frm.add_custom_button(__('Sales Order'), - function() { + function () { erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_schedule", source_doctype: "Sales Order", @@ -68,52 +64,107 @@ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({ }); }, __("Get Items From")); } else if (this.frm.doc.docstatus === 1) { - this.frm.add_custom_button(__('Create Maintenance Visit'), function() { - frappe.model.open_mapped_doc({ - method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit", - source_name: me.frm.doc.name, - frm: me.frm + let schedules = me.frm.doc.schedules; + let flag = schedules.some(schedule => schedule.completion_status === "Pending"); + if (flag) { + this.frm.add_custom_button(__('Maintenance Visit'), function () { + let options = ""; + + me.frm.call('get_pending_data', {data_type: "items"}).then(r => { + options = r.message; + + let schedule_id = ""; + let d = new frappe.ui.Dialog({ + title: __("Enter Visit Details"), + fields: [{ + fieldtype: "Select", + fieldname: "item_name", + label: __("Item Name"), + options: options, + reqd: 1, + onchange: function () { + let field = d.get_field("scheduled_date"); + me.frm.call('get_pending_data', + { + item_name: this.value, + data_type: "date" + }).then(r => { + field.df.options = r.message; + field.refresh(); + }); + } + }, + { + label: __('Scheduled Date'), + fieldname: 'scheduled_date', + fieldtype: 'Select', + options: "", + reqd: 1, + onchange: function () { + let field = d.get_field('item_name'); + me.frm.call( + 'get_pending_data', + { + item_name: field.value, + s_date: this.value, + data_type: "id" + }).then(r => { + schedule_id = r.message; + }); + } + }, + ], + primary_action_label: 'Create Visit', + primary_action(values) { + frappe.call({ + method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit", + args: { + item_name: values.item_name, + s_id: schedule_id, + source_name: me.frm.doc.name, + }, + callback: function (r) { + if (!r.exc) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }); + d.hide(); + } + }); + d.show(); }); - }, __('Create')); + }, __('Create')); + } } }, - start_date: function(doc, cdt, cdn) { + start_date: function (doc, cdt, cdn) { this.set_no_of_visits(doc, cdt, cdn); }, - end_date: function(doc, cdt, cdn) { + end_date: function (doc, cdt, cdn) { this.set_no_of_visits(doc, cdt, cdn); }, - periodicity: function(doc, cdt, cdn) { + periodicity: function (doc, cdt, cdn) { + this.set_no_of_visits(doc, cdt, cdn); + + }, + no_of_visits: function (doc, cdt, cdn) { this.set_no_of_visits(doc, cdt, cdn); }, - set_no_of_visits: function(doc, cdt, cdn) { + set_no_of_visits: function (doc, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); - - if (item.start_date && item.end_date && item.periodicity) { - if(item.start_date > item.end_date) { - frappe.msgprint(__("Row {0}:Start Date must be before End Date", [item.idx])); - return; - } - - var date_diff = frappe.datetime.get_diff(item.end_date, item.start_date) + 1; - - var days_in_period = { - "Weekly": 7, - "Monthly": 30, - "Quarterly": 91, - "Half Yearly": 182, - "Yearly": 365 - } - - var no_of_visits = cint(date_diff / days_in_period[item.periodicity]); - frappe.model.set_value(item.doctype, item.name, "no_of_visits", no_of_visits); + let me = this; + if (item.start_date && item.periodicity) { + me.frm.call('validate_end_date_visits'); + } }, }); -$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({frm: cur_frm})); +$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({ frm: cur_frm })); diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json index 606d22f52b..4f89a679c8 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json @@ -1,852 +1,264 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2013-01-10 16:34:30", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, + "actions": [], + "autoname": "naming_series:", + "creation": "2013-01-10 16:34:30", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "customer_details", + "naming_series", + "customer", + "column_break0", + "status", + "transaction_date", + "items_section", + "items", + "schedule", + "generate_schedule", + "schedules", + "contact_info", + "customer_name", + "contact_person", + "contact_mobile", + "contact_email", + "contact_display", + "column_break_17", + "customer_address", + "address_display", + "territory", + "customer_group", + "company", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-user", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer_details", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break", + "options": "fa fa-user" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "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": "Series", - "length": 0, - "no_copy": 1, - "options": "MAT-MSH-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "MAT-MSH-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, { - "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": 1, - "label": "Customer", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer", - "oldfieldtype": "Link", - "options": "Customer", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Customer", + "oldfieldname": "customer", + "oldfieldtype": "Link", + "options": "Customer", + "print_hide": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break0", - "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, - "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break0", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Draft", - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Status", - "length": 0, - "no_copy": 1, - "oldfieldname": "status", - "oldfieldtype": "Select", - "options": "\nDraft\nSubmitted\nCancelled", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "oldfieldname": "status", + "oldfieldtype": "Select", + "options": "\nDraft\nSubmitted\nCancelled", + "read_only": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "transaction_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": "Transaction Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "transaction_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "transaction_date", + "fieldtype": "Date", + "label": "Transaction Date", + "oldfieldname": "transaction_date", + "oldfieldtype": "Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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": "", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "items_section", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break", + "options": "fa fa-shopping-cart" + }, { - "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, - "oldfieldname": "item_maintenance_detail", - "oldfieldtype": "Table", - "options": "Maintenance Schedule Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "oldfieldname": "item_maintenance_detail", + "oldfieldtype": "Table", + "options": "Maintenance Schedule Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "schedule", - "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": "Schedule", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-time", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "schedule", + "fieldtype": "Section Break", + "label": "Schedule", + "oldfieldtype": "Section Break", + "options": "fa fa-time" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "generate_schedule", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Generate Schedule", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Button", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "generate_schedule", + "fieldtype": "Button", + "label": "Generate Schedule", + "oldfieldtype": "Button" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "schedules", - "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": "Schedules", - "length": 0, - "no_copy": 0, - "oldfieldname": "schedules", - "oldfieldtype": "Table", - "options": "Maintenance Schedule Detail", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "schedules", + "fieldtype": "Table", + "label": "Schedules", + "oldfieldname": "schedules", + "oldfieldtype": "Table", + "options": "Maintenance Schedule Detail" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_info", - "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": "Contact Info", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact_info", + "fieldtype": "Section Break", + "label": "Contact Info" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "depends_on": "customer", - "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": 0, - "label": "Customer Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "bold": 1, + "depends_on": "customer", + "fieldname": "customer_name", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Customer Name", + "oldfieldname": "customer_name", + "oldfieldtype": "Data", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "customer", - "fieldname": "contact_person", - "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": "Contact Person", - "length": 0, - "no_copy": 0, - "options": "Contact", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "customer", + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Contact Person", + "options": "Contact", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "customer", - "fieldname": "contact_mobile", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mobile No", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "customer", + "fieldname": "contact_mobile", + "fieldtype": "Data", + "hidden": 1, + "label": "Mobile No", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "customer", - "fieldname": "contact_email", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Email", - "length": 0, - "no_copy": 0, - "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 - }, + "depends_on": "customer", + "fieldname": "contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Contact Email", + "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": "contact_display", - "fieldtype": "Small Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact_display", + "fieldtype": "Small Text", + "hidden": 1, + "in_global_search": 1, + "label": "Contact", + "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_17", - "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, - "print_hide": 0, - "print_hide_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_17", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "customer", - "fieldname": "customer_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": "Customer Address", - "length": 0, - "no_copy": 0, - "options": "Address", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "customer", + "fieldname": "customer_address", + "fieldtype": "Link", + "label": "Customer Address", + "options": "Address", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_display", - "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": "Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_display", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Address", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "customer", - "description": "", - "fieldname": "territory", - "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": "Territory", - "length": 0, - "no_copy": 0, - "oldfieldname": "territory", - "oldfieldtype": "Link", - "options": "Territory", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "customer", + "fieldname": "territory", + "fieldtype": "Link", + "label": "Territory", + "oldfieldname": "territory", + "oldfieldtype": "Link", + "options": "Territory" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "customer", - "description": "", - "fieldname": "customer_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Group", - "length": 0, - "no_copy": 0, - "options": "Customer Group", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "customer", + "fieldname": "customer_group", + "fieldtype": "Link", + "label": "Customer Group", + "options": "Customer Group" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "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, - "oldfieldname": "company", - "oldfieldtype": "Link", - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "oldfieldname": "company", + "oldfieldtype": "Link", + "options": "Company", + "remember_last_selected_value": 1, + "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": "Maintenance Schedule", - "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": "Maintenance Schedule", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-calendar", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Maintenance", - "name": "Maintenance Schedule", - "owner": "Administrator", + ], + "icon": "fa fa-calendar", + "idx": 1, + "is_submittable": 1, + "links": [ + { + "group": "Visits", + "link_doctype": "Maintenance Visit", + "link_fieldname": "maintenance_schedule" + } + ], + "modified": "2021-05-27 16:05:10.746465", + "modified_by": "Administrator", + "module": "Maintenance", + "name": "Maintenance Schedule", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Maintenance Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Maintenance Manager", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "status,customer,customer_name", - "show_name_in_global_search": 0, - "sort_order": "DESC", - "timeline_field": "customer", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "search_fields": "status,customer,customer_name", + "sort_field": "modified", + "sort_order": "DESC", + "timeline_field": "customer" } \ No newline at end of file diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 0aefe19c8d..d6e42f3ee1 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -4,12 +4,13 @@ from __future__ import unicode_literals import frappe -from frappe.utils import add_days, getdate, cint, cstr +from frappe.utils import add_days, getdate, cint, cstr, date_diff, formatdate from frappe import throw, _ from erpnext.utilities.transaction_base import TransactionBase, delete_events from erpnext.stock.utils import get_valid_serial_nos from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos class MaintenanceSchedule(TransactionBase): @frappe.whitelist() @@ -32,8 +33,40 @@ class MaintenanceSchedule(TransactionBase): child.idx = count count = count + 1 child.sales_person = d.sales_person + child.completion_status = "Pending" + child.item_reference = d.name + + @frappe.whitelist() + def validate_end_date_visits(self): + days_in_period = { + "Weekly": 7, + "Monthly": 30, + "Quarterly": 91, + "Half Yearly": 182, + "Yearly": 365 + } + for item in self.items: + if item.periodicity and item.start_date: + if not item.end_date: + if item.no_of_visits: + item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity]) + else: + item.end_date = add_days(item.start_date, days_in_period[item.periodicity]) + + diff = date_diff(item.end_date, item.start_date) + 1 + no_of_visits = cint(diff / days_in_period[item.periodicity]) + + if not item.no_of_visits or item.no_of_visits == 0: + item.end_date = add_days(item.start_date, days_in_period[item.periodicity]) + diff = date_diff(item.end_date, item.start_date) + 1 + item.no_of_visits = cint(diff / days_in_period[item.periodicity]) + + elif item.no_of_visits > no_of_visits: + item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity]) + + elif item.no_of_visits < no_of_visits: + item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity]) - self.save() def on_submit(self): if not self.get('schedules'): @@ -58,9 +91,10 @@ class MaintenanceSchedule(TransactionBase): if no_email_sp: frappe.msgprint( - frappe._("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format( + _("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format( self.owner, "
" + "
".join(no_email_sp) - )) + ) + ) scheduled_date = frappe.db.sql("""select scheduled_date from `tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and @@ -106,7 +140,7 @@ class MaintenanceSchedule(TransactionBase): if employee: holiday_list = get_holiday_list_for_employee(employee) else: - holiday_list = frappe.get_cached_value('Company', self.company, "default_holiday_list") + holiday_list = frappe.get_cached_value('Company', self.company, "default_holiday_list") holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` where parent=%s''', holiday_list) @@ -135,8 +169,7 @@ class MaintenanceSchedule(TransactionBase): } if date_diff < days_in_period[d.periodicity]: - throw(_("Row {0}: To set {1} periodicity, difference between from and to date \ - must be greater than or equal to {2}") + throw(_("Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}") .format(d.idx, d.periodicity, days_in_period[d.periodicity])) def validate_maintenance_detail(self): @@ -166,13 +199,15 @@ class MaintenanceSchedule(TransactionBase): throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order)) def validate(self): + self.validate_end_date_visits() self.validate_maintenance_detail() self.validate_dates_with_periodicity() self.validate_sales_order() + self.generate_schedule() def on_update(self): frappe.db.set(self, 'status', 'Draft') - + def update_amc_date(self, serial_nos, amc_expiry_date=None): for serial_no in serial_nos: serial_no_doc = frappe.get_doc("Serial No", serial_no) @@ -202,8 +237,8 @@ class MaintenanceSchedule(TransactionBase): if not sr_details.warehouse and sr_details.delivery_date and \ getdate(sr_details.delivery_date) >= getdate(amc_start_date): - throw(_("Maintenance start date can not be before delivery date for Serial No {0}") - .format(serial_no)) + throw(_("Maintenance start date can not be before delivery date for Serial No {0}") + .format(serial_no)) def validate_schedule(self): item_lst1 =[] @@ -245,13 +280,50 @@ class MaintenanceSchedule(TransactionBase): def on_trash(self): delete_events(self.doctype, self.name) + @frappe.whitelist() + def get_pending_data(self, data_type, s_date=None, item_name=None): + if data_type == "date": + dates = "" + for schedule in self.schedules: + if schedule.item_name == item_name and schedule.completion_status == "Pending": + dates = dates + "\n" + formatdate(schedule.scheduled_date, "dd-MM-yyyy") + return dates + elif data_type == "items": + items = "" + for item in self.items: + for schedule in self.schedules: + if item.item_name == schedule.item_name and schedule.completion_status == "Pending": + items = items + "\n" + item.item_name + break + return items + elif data_type == "id": + for schedule in self.schedules: + if schedule.item_name == item_name and s_date == formatdate(schedule.scheduled_date, "dd-mm-yyyy"): + return schedule.name + @frappe.whitelist() -def make_maintenance_visit(source_name, target_doc=None): +def update_serial_nos(s_id): + serial_nos = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'serial_no') + if serial_nos: + serial_nos = get_serial_nos(serial_nos) + return serial_nos + else: + return False + +@frappe.whitelist() +def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None): from frappe.model.mapper import get_mapped_doc - def update_status(source, target, parent): + 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(source, target, parent): + sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person') + target.service_person = sales_person + target.serial_no = '' + doclist = get_mapped_doc("Maintenance Schedule", source_name, { "Maintenance Schedule": { "doctype": "Maintenance Visit", @@ -261,15 +333,12 @@ def make_maintenance_visit(source_name, target_doc=None): "validation": { "docstatus": ["=", 1] }, - "postprocess": update_status + "postprocess": update_status_and_detail }, "Maintenance Schedule Item": { "doctype": "Maintenance Visit Purpose", - "field_map": { - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", - "sales_person": "service_person" - } + "condition": lambda doc: doc.item_name == item_name, + "postprocess": update_sales } }, 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 3c307e920f..09981bad05 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -2,7 +2,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals -from frappe.utils.data import get_datetime, add_days +from frappe.utils.data import add_days, today, formatdate +from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import make_maintenance_visit import frappe import unittest @@ -21,7 +22,57 @@ class TestMaintenanceSchedule(unittest.TestCase): ms.cancel() events_after_cancel = get_events(ms) self.assertTrue(len(events_after_cancel) == 0) + + def test_make_schedule(self): + ms = make_maintenance_schedule() + ms.save() + i = ms.items[0] + expected_dates = [] + expected_end_date = add_days(i.start_date, i.no_of_visits * 7) + self.assertEqual(i.end_date, expected_end_date) + i.no_of_visits = 2 + ms.save() + expected_end_date = add_days(i.start_date, i.no_of_visits * 7) + self.assertEqual(i.end_date, expected_end_date) + + items = ms.get_pending_data(data_type = "items") + items = items.split('\n') + items.pop(0) + expected_items = ['_Test Item'] + self.assertTrue(items, expected_items) + + # "dates" contains all generated schedule dates + dates = ms.get_pending_data(data_type = "date", item_name = i.item_name) + dates = dates.split('\n') + dates.pop(0) + expected_dates.append(formatdate(add_days(i.start_date, 7), "dd-MM-yyyy")) + expected_dates.append(formatdate(add_days(i.start_date, 14), "dd-MM-yyyy")) + + # test for generated schedule dates + self.assertEqual(dates, expected_dates) + + ms.submit() + s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1]) + test = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id) + visit = frappe.new_doc('Maintenance Visit') + visit = test + visit.maintenance_schedule = ms.name + visit.maintenance_schedule_detail = s_id + visit.completion_status = "Partially Completed" + visit.set('purposes', [{ + 'item_code': i.item_code, + 'description': "test", + 'work_done': "test", + 'service_person': "Sales Team", + }]) + visit.save() + visit.submit() + ms = frappe.get_doc('Maintenance Schedule', ms.name) + + #checks if visit status is back updated in schedule + self.assertTrue(ms.schedules[1].completion_status, "Partially Completed") + def get_events(ms): return frappe.get_all("Event Participants", filters={ "reference_doctype": ms.doctype, @@ -33,12 +84,11 @@ def make_maintenance_schedule(): ms = frappe.new_doc("Maintenance Schedule") ms.company = "_Test Company" ms.customer = "_Test Customer" - ms.transaction_date = get_datetime() + ms.transaction_date = today() ms.append("items", { "item_code": "_Test Item", - "start_date": get_datetime(), - "end_date": add_days(get_datetime(), 32), + "start_date": today(), "periodicity": "Weekly", "no_of_visits": 4, "sales_person": "Sales Team", diff --git a/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json b/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json index 7cd3086155..8ccef6a817 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json +++ b/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json @@ -1,222 +1,137 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-02-22 01:28:05", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:28:05", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "column_break_3", + "scheduled_date", + "actual_date", + "section_break_6", + "sales_person", + "column_break_8", + "completion_status", + "section_break_10", + "serial_no", + "item_reference" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "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 Code", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "read_only": 1, + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_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": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Item Name", + "oldfieldname": "item_name", + "oldfieldtype": "Data", + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "scheduled_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": "Scheduled Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "scheduled_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "scheduled_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Scheduled Date", + "oldfieldname": "scheduled_date", + "oldfieldtype": "Date", + "reqd": 1, + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "actual_date", - "fieldtype": "Date", - "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": "Actual Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "actual_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "actual_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Actual Date", + "no_copy": 1, + "oldfieldname": "actual_date", + "oldfieldtype": "Date", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_person", - "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": "Sales Person", - "length": 0, - "no_copy": 0, - "oldfieldname": "incharge_name", - "oldfieldtype": "Link", - "options": "Sales Person", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "columns": 2, + "fieldname": "sales_person", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Person", + "oldfieldname": "incharge_name", + "oldfieldtype": "Link", + "options": "Sales Person", + "read_only_depends_on": "eval:doc.completion_status != \"Pending\"" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "serial_no", - "fieldtype": "Small Text", - "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": "Serial No", - "length": 0, - "no_copy": 0, - "oldfieldname": "serial_no", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "160px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "serial_no", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Serial No", + "oldfieldname": "serial_no", + "oldfieldtype": "Small Text", + "print_width": "160px", + "read_only": 1, "width": "160px" + }, + { + "columns": 2, + "fieldname": "completion_status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Completion Status", + "options": "Pending\nPartially Completed\nFully Completed", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "item_reference", + "fieldtype": "Link", + "hidden": 1, + "label": "Item Reference", + "options": "Maintenance Schedule Item", + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-02-17 17:05:44.644663", - "modified_by": "Administrator", - "module": "Maintenance", - "name": "Maintenance Schedule Detail", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 1, - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2021-05-27 16:07:25.905015", + "modified_by": "Administrator", + "module": "Maintenance", + "name": "Maintenance Schedule Detail", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/maintenance/doctype/maintenance_schedule_item/maintenance_schedule_item.json b/erpnext/maintenance/doctype/maintenance_schedule_item/maintenance_schedule_item.json index b371dfc4f5..3dacdead62 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule_item/maintenance_schedule_item.json +++ b/erpnext/maintenance/doctype/maintenance_schedule_item/maintenance_schedule_item.json @@ -1,431 +1,160 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-02-22 01:28:05", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:28:05", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "description", + "column_break_4", + "start_date", + "end_date", + "periodicity", + "schedule_details", + "no_of_visits", + "column_break_10", + "sales_person", + "reference", + "serial_no", + "sales_order" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "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 Code", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "columns": 1, "fetch_from": "item_code.item_name", - "fieldname": "item_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": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_name", - "oldfieldtype": "Data", - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Name", + "oldfieldname": "item_name", + "oldfieldtype": "Data", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "item_code.description", - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Data", - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "300px", - "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": "description", + "fieldtype": "Text Editor", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Data", + "print_width": "300px", + "read_only": 1, "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "schedule_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "schedule_details", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "start_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": "Start Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "start_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Start Date", + "oldfieldname": "start_date", + "oldfieldtype": "Date", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "End Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "end_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "oldfieldname": "end_date", + "oldfieldtype": "Date", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "periodicity", - "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": "Periodicity", - "length": 0, - "no_copy": 0, - "oldfieldname": "periodicity", - "oldfieldtype": "Select", - "options": "\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly\nRandom", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 1, + "fieldname": "periodicity", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Periodicity", + "oldfieldname": "periodicity", + "oldfieldtype": "Select", + "options": "\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly\nRandom" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "no_of_visits", - "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": "No of Visits", - "length": 0, - "no_copy": 0, - "oldfieldname": "no_of_visits", - "oldfieldtype": "Int", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 1, + "fieldname": "no_of_visits", + "fieldtype": "Int", + "in_list_view": 1, + "label": "No of Visits", + "oldfieldname": "no_of_visits", + "oldfieldtype": "Int", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_person", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Person", - "length": 0, - "no_copy": 0, - "oldfieldname": "incharge_name", - "oldfieldtype": "Link", - "options": "Sales Person", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "sales_person", + "fieldtype": "Link", + "label": "Sales Person", + "oldfieldname": "incharge_name", + "oldfieldtype": "Link", + "options": "Sales Person" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference", - "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": "Reference", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "reference", + "fieldtype": "Section Break", + "label": "Reference" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "serial_no", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Serial No", - "length": 0, - "no_copy": 0, - "oldfieldname": "serial_no", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No", + "oldfieldname": "serial_no", + "oldfieldtype": "Small Text" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_order", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Order", - "length": 0, - "no_copy": 1, - "oldfieldname": "prevdoc_docname", - "oldfieldtype": "Data", - "options": "Sales Order", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "fieldname": "sales_order", + "fieldtype": "Link", + "label": "Sales Order", + "no_copy": 1, + "oldfieldname": "prevdoc_docname", + "oldfieldtype": "Data", + "options": "Sales Order", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "search_index": 1, "width": "150px" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-05-16 22:43:14.260729", - "modified_by": "Administrator", - "module": "Maintenance", - "name": "Maintenance Schedule Item", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 0, - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2021-04-15 16:09:47.311994", + "modified_by": "Administrator", + "module": "Maintenance", + "name": "Maintenance Schedule Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index 4cbb02a5b3..d6105c657e 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -2,39 +2,62 @@ // 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) { + refresh: function (frm) { //filters for serial_no based on item_code - frm.set_query('serial_no', 'purposes', function(frm, cdt, cdn) { + frm.set_query('serial_no', 'purposes', function (frm, cdt, cdn) { let item = locals[cdt][cdn]; - return { - filters: { - 'item_code': item.item_code - } - }; + 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) { + 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) { + onload: function (frm, cdt, cdn) { + let item = locals[cdt][cdn]; + if (frm.maintenance_type == 'Scheduled') { + let schedule_id = item.purposes[0].prevdoc_detail_docname; + frappe.call({ + method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos", + args: { + s_id: schedule_id + }, + callback: function (r) { + serial_nos = r.message; + } + }); + } + if (!frm.doc.status) { - frm.set_value({status:'Draft'}); + frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { - frm.set_value({mntc_date: frappe.datetime.get_today()}); + frm.set_value({ mntc_date: frappe.datetime.get_today() }); } }, - customer: function(frm) { + customer: function (frm) { erpnext.utils.get_party_details(frm); }, - customer_address: function(frm) { + customer_address: function (frm) { erpnext.utils.get_address_display(frm, 'customer_address', 'address_display'); }, - contact_person: function(frm) { + contact_person: function (frm) { erpnext.utils.get_contact_details(frm); } @@ -42,14 +65,14 @@ frappe.ui.form.on('Maintenance Visit', { // TODO commonify this code erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ - refresh: function() { - frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} + refresh: function () { + frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer' }; var me = this; - if (this.frm.doc.docstatus===0) { + if (this.frm.doc.docstatus === 0) { this.frm.add_custom_button(__('Maintenance Schedule'), - function() { + function () { erpnext.utils.map_current_doc({ method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit", source_doctype: "Maintenance Schedule", @@ -64,7 +87,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ }) }, __("Get Items From")); this.frm.add_custom_button(__('Warranty Claim'), - function() { + function () { erpnext.utils.map_current_doc({ method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", source_doctype: "Warranty Claim", @@ -80,7 +103,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ }) }, __("Get Items From")); this.frm.add_custom_button(__('Sales Order'), - function() { + function () { erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_visit", source_doctype: "Sales Order", @@ -99,4 +122,4 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ }, }); -$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({frm: cur_frm})); \ No newline at end of file +$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({ frm: cur_frm })); \ No newline at end of file diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json index 32bfa0e324..ec32239518 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json @@ -1,1042 +1,324 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2013-01-10 16:34:31", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, + "actions": [], + "autoname": "naming_series:", + "creation": "2013-01-10 16:34:31", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "customer_details", + "column_break0", + "naming_series", + "customer", + "customer_name", + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "maintenance_schedule", + "maintenance_schedule_detail", + "column_break1", + "mntc_date", + "mntc_time", + "maintenance_details", + "completion_status", + "column_break_14", + "maintenance_type", + "section_break0", + "purposes", + "more_info", + "customer_feedback", + "col_break3", + "status", + "amended_from", + "company", + "contact_info_section", + "customer_address", + "contact_person", + "col_break4", + "territory", + "customer_group" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-user", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer_details", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break", + "options": "fa fa-user" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break0", - "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, - "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break0", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "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": "Series", - "length": 0, - "no_copy": 1, - "options": "MAT-MVS-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "MAT-MVS-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer", - "oldfieldtype": "Link", - "options": "Customer", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer", + "fieldtype": "Link", + "in_global_search": 1, + "label": "Customer", + "oldfieldname": "customer", + "oldfieldtype": "Link", + "options": "Customer", + "print_hide": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_name", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "bold": 1, + "fieldname": "customer_name", + "fieldtype": "Data", + "hidden": 1, + "in_global_search": 1, + "label": "Customer Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_display", - "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": "Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_display", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Address", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_display", - "fieldtype": "Small Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact_display", + "fieldtype": "Small Text", + "hidden": 1, + "in_global_search": 1, + "label": "Contact", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_mobile", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mobile No", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact_mobile", + "fieldtype": "Data", + "hidden": 1, + "label": "Mobile No", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_email", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Contact Email", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break1", - "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, - "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "fieldname": "column_break1", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "mntc_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": "Maintenance Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "mntc_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "mntc_date", + "fieldtype": "Date", + "label": "Maintenance Date", + "no_copy": 1, + "oldfieldname": "mntc_date", + "oldfieldtype": "Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mntc_time", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maintenance Time", - "length": 0, - "no_copy": 1, - "oldfieldname": "mntc_time", - "oldfieldtype": "Time", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "mntc_time", + "fieldtype": "Time", + "label": "Maintenance Time", + "no_copy": 1, + "oldfieldname": "mntc_time", + "oldfieldtype": "Time" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maintenance_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-wrench", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "maintenance_details", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break", + "options": "fa fa-wrench" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "completion_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": 1, - "label": "Completion Status", - "length": 0, - "no_copy": 0, - "oldfieldname": "completion_status", - "oldfieldtype": "Select", - "options": "\nPartially Completed\nFully Completed", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "completion_status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Completion Status", + "oldfieldname": "completion_status", + "oldfieldtype": "Select", + "options": "\nPartially Completed\nFully Completed", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_14", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_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_14", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Unscheduled", - "fieldname": "maintenance_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Maintenance Type", - "length": 0, - "no_copy": 0, - "oldfieldname": "maintenance_type", - "oldfieldtype": "Select", - "options": "\nScheduled\nUnscheduled\nBreakdown", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Unscheduled", + "fieldname": "maintenance_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Maintenance Type", + "oldfieldname": "maintenance_type", + "oldfieldtype": "Select", + "options": "\nScheduled\nUnscheduled\nBreakdown", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break0", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-wrench", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break0", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break", + "options": "fa fa-wrench" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purposes", - "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": "Purposes", - "length": 0, - "no_copy": 0, - "oldfieldname": "maintenance_visit_details", - "oldfieldtype": "Table", - "options": "Maintenance Visit Purpose", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "purposes", + "fieldtype": "Table", + "label": "Purposes", + "oldfieldname": "maintenance_visit_details", + "oldfieldtype": "Table", + "options": "Maintenance Visit Purpose", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "more_info", - "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": "More Information", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-file-text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "more_info", + "fieldtype": "Section Break", + "label": "More Information", + "oldfieldtype": "Section Break", + "options": "fa fa-file-text" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_feedback", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Feedback", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer_feedback", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer_feedback", + "fieldtype": "Small Text", + "label": "Customer Feedback", + "oldfieldname": "customer_feedback", + "oldfieldtype": "Small Text" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break3", - "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, - "print_hide": 0, - "print_hide_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": "col_break3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Draft", - "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": 1, - "oldfieldname": "status", - "oldfieldtype": "Data", - "options": "\nDraft\nCancelled\nSubmitted", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "oldfieldname": "status", + "oldfieldtype": "Data", + "options": "\nDraft\nCancelled\nSubmitted", + "read_only": 1, + "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": 1, - "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, - "oldfieldname": "amended_from", - "oldfieldtype": "Data", - "options": "Maintenance Visit", - "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", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "oldfieldname": "amended_from", + "oldfieldtype": "Data", + "options": "Maintenance Visit", + "print_hide": 1, + "read_only": 1, "width": "150px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "oldfieldname": "company", - "oldfieldtype": "Select", - "options": "Company", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "oldfieldname": "company", + "oldfieldtype": "Select", + "options": "Company", + "print_hide": 1, + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "customer", - "fieldname": "contact_info_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": "Contact Info", - "length": 0, - "no_copy": 0, - "options": "fa fa-bullhorn", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "customer", + "fieldname": "contact_info_section", + "fieldtype": "Section Break", + "label": "Contact Info", + "options": "fa fa-bullhorn" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_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": "Customer Address", - "length": 0, - "no_copy": 0, - "options": "Address", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer_address", + "fieldtype": "Link", + "label": "Customer Address", + "options": "Address", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_person", - "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": "Contact Person", - "length": 0, - "no_copy": 0, - "options": "Contact", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Contact Person", + "options": "Contact", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break4", - "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, - "print_hide": 0, - "print_hide_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": "col_break4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "territory", - "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": "Territory", - "length": 0, - "no_copy": 0, - "options": "Territory", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "territory", + "fieldtype": "Link", + "label": "Territory", + "options": "Territory", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "customer_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Group", - "length": 0, - "no_copy": 0, - "options": "Customer Group", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 + "fieldname": "customer_group", + "fieldtype": "Link", + "label": "Customer Group", + "options": "Customer Group", + "print_hide": 1 + }, + { + "fieldname": "maintenance_schedule", + "fieldtype": "Link", + "label": "Maintenance Schedule", + "options": "Maintenance Schedule", + "read_only": 1 + }, + { + "fieldname": "maintenance_schedule_detail", + "fieldtype": "Link", + "hidden": 1, + "label": "Maintenance Schedule Detail", + "options": "Maintenance Schedule Detail" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-file-text", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Maintenance", - "name": "Maintenance Visit", - "owner": "Administrator", + ], + "icon": "fa fa-file-text", + "idx": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-05-27 16:06:17.352572", + "modified_by": "Administrator", + "module": "Maintenance", + "name": "Maintenance Visit", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Maintenance User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Maintenance User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "status,maintenance_type,customer,customer_name,mntc_date,company", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "timeline_field": "customer", - "title_field": "customer_name", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "search_fields": "status,maintenance_type,customer,customer_name,mntc_date,company", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "timeline_field": "customer", + "title_field": "customer_name" } \ No newline at end of file diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py index 2f2ad00e02..7fffc942a0 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils import get_datetime from erpnext.utilities.transaction_base import TransactionBase @@ -16,44 +17,62 @@ 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_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)) + def validate(self): self.validate_serial_no() + self.validate_maintenance_date() + + def update_completion_status(self): + 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) def update_customer_issue(self, flag): - for d in self.get('purposes'): - if d.prevdoc_docname and d.prevdoc_doctype == 'Warranty Claim' : - if flag==1: - mntc_date = self.mntc_date - service_person = d.service_person - work_done = d.work_done - status = "Open" - if self.completion_status == 'Fully Completed': - status = 'Closed' - elif self.completion_status == 'Partially Completed': - status = 'Work In Progress' - else: - nm = frappe.db.sql("select t1.name, t1.mntc_date, t2.service_person, t2.work_done from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.completion_status = 'Partially Completed' and t2.prevdoc_docname = %s and t1.name!=%s and t1.docstatus = 1 order by t1.name desc limit 1", (d.prevdoc_docname, self.name)) - - if nm: - status = 'Work In Progress' - mntc_date = nm and nm[0][1] or '' - service_person = nm and nm[0][2] or '' - work_done = nm and nm[0][3] or '' + if not self.maintenance_schedule: + for d in self.get('purposes'): + if d.prevdoc_docname and d.prevdoc_doctype == 'Warranty Claim' : + if flag==1: + mntc_date = self.mntc_date + service_person = d.service_person + work_done = d.work_done + status = "Open" + if self.completion_status == 'Fully Completed': + status = 'Closed' + elif self.completion_status == 'Partially Completed': + status = 'Work In Progress' else: - status = 'Open' - mntc_date = None - service_person = None - work_done = None + nm = frappe.db.sql("select t1.name, t1.mntc_date, t2.service_person, t2.work_done from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.completion_status = 'Partially Completed' and t2.prevdoc_docname = %s and t1.name!=%s and t1.docstatus = 1 order by t1.name desc limit 1", (d.prevdoc_docname, self.name)) - wc_doc = frappe.get_doc('Warranty Claim', d.prevdoc_docname) - wc_doc.update({ - 'resolution_date': mntc_date, - 'resolved_by': service_person, - 'resolution_details': work_done, - 'status': status - }) + if nm: + status = 'Work In Progress' + mntc_date = nm and nm[0][1] or '' + service_person = nm and nm[0][2] or '' + work_done = nm and nm[0][3] or '' + else: + status = 'Open' + mntc_date = None + service_person = None + work_done = None - wc_doc.db_update() + wc_doc = frappe.get_doc('Warranty Claim', d.prevdoc_docname) + wc_doc.update({ + 'resolution_date': mntc_date, + 'resolved_by': service_person, + 'resolution_details': work_done, + 'status': status + }) + + wc_doc.db_update() def check_if_last_visit(self): """check if last maintenance visit against same sales order/ Warranty Claim""" @@ -77,6 +96,8 @@ 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() def on_cancel(self): self.check_if_last_visit() diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json index 467441d841..158f143ae8 100644 --- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json +++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-02-22 01:28:06", "doctype": "DocType", @@ -8,14 +9,15 @@ "field_order": [ "item_code", "item_name", + "column_break_3", + "service_person", "serial_no", + "section_break_6", "description", "work_details", - "service_person", "work_done", "prevdoc_doctype", - "prevdoc_docname", - "prevdoc_detail_docname" + "prevdoc_docname" ], "fields": [ { @@ -62,6 +64,8 @@ "fieldtype": "Section Break" }, { + "fetch_from": "prevdoc_detail_docname.sales_person", + "fetch_if_empty": 1, "fieldname": "service_person", "fieldtype": "Link", "in_list_view": 1, @@ -83,49 +87,30 @@ { "fieldname": "prevdoc_doctype", "fieldtype": "Link", + "hidden": 1, "label": "Document Type", - "no_copy": 1, - "oldfieldname": "prevdoc_doctype", - "oldfieldtype": "Data", - "options": "DocType", - "print_hide": 1, - "print_width": "150px", - "read_only": 1, - "report_hide": 1, - "width": "150px" + "options": "DocType" }, { "fieldname": "prevdoc_docname", "fieldtype": "Dynamic Link", + "hidden": 1, "label": "Against Document No", - "no_copy": 1, - "oldfieldname": "prevdoc_docname", - "oldfieldtype": "Data", - "options": "prevdoc_doctype", - "print_hide": 1, - "print_width": "160px", - "read_only": 1, - "report_hide": 1, - "width": "160px" + "options": "prevdoc_doctype" }, { - "fieldname": "prevdoc_detail_docname", - "fieldtype": "Data", - "hidden": 1, - "label": "Against Document Detail No", - "no_copy": 1, - "oldfieldname": "prevdoc_detail_docname", - "oldfieldtype": "Data", - "print_hide": 1, - "print_width": "160px", - "read_only": 1, - "report_hide": 1, - "width": "160px" + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" } ], "idx": 1, "istable": 1, - "modified": "2020-09-18 17:26:09.703215", + "links": [], + "modified": "2021-05-27 17:47:21.474282", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit Purpose", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index fb26062566..d764db33f8 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -433,7 +433,8 @@ def make_material_request(source_name, target_doc=None): def make_stock_entry(source_name, target_doc=None): def update_item(obj, target, source_parent): target.t_warehouse = source_parent.wip_warehouse - target.conversion_factor = 1 + if not target.conversion_factor: + target.conversion_factor = 1 def set_missing_values(source, target): target.purpose = "Material Transfer for Manufacture" diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 288c1d0cd6..64d584118f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -211,16 +211,27 @@ frappe.ui.form.on('Production Plan', { }); }, - get_items: function(frm) { + get_items: function (frm) { + frm.clear_table('prod_plan_references'); + frappe.call({ method: "get_items", freeze: true, doc: frm.doc, - callback: function() { + callback: function () { refresh_field('po_items'); } }); }, + combine_items: function (frm) { + frm.clear_table('prod_plan_references'); + + frappe.call({ + method: "get_items", + freeze: true, + doc: frm.doc, + }); + }, get_items_for_mr: function(frm) { if (!frm.doc.for_warehouse) { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index f11470086a..1c0dde227c 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -28,7 +28,10 @@ "material_requests", "select_items_to_manufacture_section", "get_items", + "combine_items", "po_items", + "section_break_25", + "prod_plan_references", "material_request_planning", "include_non_stock_items", "include_subcontracted_items", @@ -316,13 +319,31 @@ "fieldname": "include_safety_stock", "fieldtype": "Check", "label": "Include Safety Stock in Required Qty Calculation" + }, + { + "default": "0", + "depends_on": "eval:doc.get_items_from == 'Sales Order'", + "fieldname": "combine_items", + "fieldtype": "Check", + "label": "Consolidate Items" + }, + { + "fieldname": "section_break_25", + "fieldtype": "Section Break" + }, + { + "fieldname": "prod_plan_references", + "fieldtype": "Table", + "hidden": 1, + "label": "Production Plan Item Reference", + "options": "Production Plan Item Reference" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-03-08 11:17:25.470147", + "modified": "2021-05-24 16:59:03.643211", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a3e23a6897..46e047654b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -96,8 +96,10 @@ class ProductionPlan(Document): @frappe.whitelist() def get_items(self): + self.set('po_items', []) if self.get_items_from == "Sales Order": - self.get_so_items() + self.get_so_items() + elif self.get_items_from == "Material Request": self.get_mr_items() @@ -165,9 +167,31 @@ class ProductionPlan(Document): self.calculate_total_planned_qty() def add_items(self, items): - self.set('po_items', []) + refs = {} for data in items: item_details = get_item_details(data.item_code) + if self.combine_items: + if item_details.bom_no in refs: + refs[item_details.bom_no]['so_details'].append({ + 'sales_order': data.parent, + 'sales_order_item': data.name, + 'qty': data.pending_qty + }) + refs[item_details.bom_no]['qty'] += data.pending_qty + continue + + else: + refs[item_details.bom_no] = { + 'qty': data.pending_qty, + 'po_item_ref': data.name, + 'so_details': [] + } + refs[item_details.bom_no]['so_details'].append({ + 'sales_order': data.parent, + 'sales_order_item': data.name, + 'qty': data.pending_qty + }) + pi = self.append('po_items', { 'include_exploded_items': 1, 'warehouse': data.warehouse, @@ -185,11 +209,28 @@ class ProductionPlan(Document): pi.sales_order = data.parent pi.sales_order_item = data.name pi.description = data.description - + elif self.get_items_from == "Material Request": pi.material_request = data.parent pi.material_request_item = data.name pi.description = data.description + + if refs: + for po_item in self.po_items: + po_item.planned_qty = refs[po_item.bom_no]['qty'] + po_item.pending_qty = refs[po_item.bom_no]['qty'] + po_item.sales_order = '' + self.add_pp_ref(refs) + + def add_pp_ref(self, refs): + for bom_no in refs: + for so_detail in refs[bom_no]['so_details']: + self.append('prod_plan_references', { + 'item_reference': refs[bom_no]['po_item_ref'], + 'sales_order': so_detail['sales_order'], + 'sales_order_item': so_detail['sales_order_item'], + 'qty': so_detail['qty'] + }) def calculate_total_planned_qty(self): self.total_planned_qty = 0 diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 27335aa204..768f99eb43 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -100,7 +100,7 @@ class TestProductionPlan(unittest.TestCase): def test_production_plan_sales_orders(self): item = 'Test Production Item 1' - so = make_sales_order(item_code=item, qty=5) + so = make_sales_order(item_code=item, qty=1) sales_order = so.name sales_order_item = so.items[0].name @@ -124,8 +124,8 @@ class TestProductionPlan(unittest.TestCase): wo_doc = frappe.get_doc('Work Order', work_order) wo_doc.update({ - 'wip_warehouse': '_Test Warehouse 1 - _TC', - 'fg_warehouse': '_Test Warehouse - _TC' + 'wip_warehouse': 'Work In Progress - _TC', + 'fg_warehouse': 'Finished Goods - _TC' }) wo_doc.submit() @@ -145,6 +145,58 @@ class TestProductionPlan(unittest.TestCase): self.assertEqual(sales_orders, []) + def test_production_plan_combine_items(self): + item = 'Test Production Item 1' + so = make_sales_order(item_code=item, qty=1) + + pln = frappe.new_doc('Production Plan') + pln.company = so.company + pln.get_items_from = 'Sales Order' + pln.append('sales_orders', { + 'sales_order': so.name, + 'sales_order_date': so.transaction_date, + 'customer': so.customer, + 'grand_total': so.grand_total + }) + so = make_sales_order(item_code=item, qty=2) + pln.append('sales_orders', { + 'sales_order': so.name, + 'sales_order_date': so.transaction_date, + 'customer': so.customer, + 'grand_total': so.grand_total + }) + pln.combine_items = 1 + pln.get_items() + pln.submit() + + self.assertTrue(pln.po_items[0].planned_qty, 3) + + pln.make_work_order() + work_order = frappe.db.get_value('Work Order', { + 'production_plan_item': pln.po_items[0].name, + 'production_plan': pln.name + }, 'name') + + wo_doc = frappe.get_doc('Work Order', work_order) + wo_doc.update({ + 'wip_warehouse': 'Work In Progress - _TC', + }) + + wo_doc.submit() + so_items = [] + for plan_reference in pln.prod_plan_references: + so_items.append(plan_reference.sales_order_item) + so_wo_qty = frappe.db.get_value('Sales Order Item', plan_reference.sales_order_item, 'work_order_qty') + self.assertEqual(so_wo_qty, plan_reference.qty) + + wo_doc.cancel() + for so_item in so_items: + so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty') + self.assertEqual(so_wo_qty, 0.0) + + latest_plan = frappe.get_doc('Production Plan', pln.name) + latest_plan.cancel() + def test_pp_to_mr_customer_provided(self): #Material Request from Production Plan for Customer Provided create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json index d0dce53437..89ab7aa0a0 100644 --- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json +++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -1,792 +1,229 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-02-22 01:27:49", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 1, + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:27:49", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "include_exploded_items", + "item_code", + "bom_no", + "planned_qty", + "column_break_6", + "make_work_order_for_sub_assembly_items", + "warehouse", + "planned_start_date", + "section_break_9", + "pending_qty", + "ordered_qty", + "produced_qty", + "column_break_17", + "description", + "stock_uom", + "reference_section", + "sales_order", + "sales_order_item", + "column_break_19", + "material_request", + "material_request_item", + "product_bundle_item", + "item_reference" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_if_empty": 0, - "fieldname": "include_exploded_items", - "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": "Include Exploded 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, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "default": "0", + "fieldname": "include_exploded_items", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Include Exploded Items" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_if_empty": 0, - "fieldname": "item_code", - "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 Code", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "print_width": "150px", + "reqd": 1, "width": "150px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_if_empty": 0, - "fieldname": "bom_no", - "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": "BOM No", - "length": 0, - "no_copy": 0, - "oldfieldname": "bom_no", - "oldfieldtype": "Link", - "options": "BOM", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "columns": 2, + "fieldname": "bom_no", + "fieldtype": "Link", + "in_list_view": 1, + "label": "BOM No", + "oldfieldname": "bom_no", + "oldfieldtype": "Link", + "options": "BOM", + "print_width": "100px", + "reqd": 1, "width": "100px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "planned_qty", - "fieldtype": "Float", - "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": "Planned Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "planned_qty", - "oldfieldtype": "Currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "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": "planned_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Planned Qty", + "oldfieldname": "planned_qty", + "oldfieldtype": "Currency", + "print_width": "100px", + "reqd": 1, "width": "100px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 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 - }, + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "description": "If enabled, system will create the work order for the exploded items against which BOM is available.", - "fetch_if_empty": 0, - "fieldname": "make_work_order_for_sub_assembly_items", - "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": "Make Work Order for Sub Assembly 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, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "description": "If enabled, system will create the work order for the exploded items against which BOM is available.", + "fieldname": "make_work_order_for_sub_assembly_items", + "fieldtype": "Check", + "label": "Make Work Order for Sub Assembly Items" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "warehouse", - "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": "For Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "For Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fetch_if_empty": 0, - "fieldname": "planned_start_date", - "fieldtype": "Datetime", - "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": "Planned Start 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 - }, + "default": "Today", + "fieldname": "planned_start_date", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Planned Start Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_9", - "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": "Quantity and Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Quantity and Description" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "pending_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Pending Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "prevdoc_reqd_qty", - "oldfieldtype": "Currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "default": "0", + "fieldname": "pending_qty", + "fieldtype": "Float", + "label": "Pending Qty", + "oldfieldname": "prevdoc_reqd_qty", + "oldfieldtype": "Currency", + "print_width": "100px", + "read_only": 1, "width": "100px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "ordered_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Ordered Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "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 - }, + "default": "0", + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "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, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "produced_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Produced Qty", - "length": 0, - "no_copy": 1, - "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 - }, + "default": "0", + "fieldname": "produced_qty", + "fieldtype": "Float", + "label": "Produced Qty", + "no_copy": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_17", - "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_17", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "200px", - "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": "description", + "fieldtype": "Text Editor", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "200px", + "read_only": 1, "width": "200px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "stock_uom", - "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": "UOM", - "length": 0, - "no_copy": 0, - "oldfieldname": "stock_uom", - "oldfieldtype": "Data", - "options": "UOM", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "80px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "UOM", + "oldfieldname": "stock_uom", + "oldfieldtype": "Data", + "options": "UOM", + "print_width": "80px", + "read_only": 1, + "reqd": 1, "width": "80px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "reference_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": "Reference", - "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": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "sales_order", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Order", - "length": 0, - "no_copy": 0, - "oldfieldname": "source_docname", - "oldfieldtype": "Data", - "options": "Sales Order", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "sales_order", + "fieldtype": "Link", + "label": "Sales Order", + "oldfieldname": "source_docname", + "oldfieldtype": "Data", + "options": "Sales Order", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "sales_order_item", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Order Item", - "length": 0, - "no_copy": 1, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "sales_order_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Sales Order Item", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_19", - "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_19", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "material_request", - "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": "Material Request", - "length": 0, - "no_copy": 0, - "options": "Material Request", - "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 - }, + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "options": "Material Request", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "material_request_item", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "material_request_item", - "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": "material_request_item", + "fieldtype": "Data", + "hidden": 1, + "label": "material_request_item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "product_bundle_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": "Product Bundle Item", - "length": 0, - "no_copy": 1, - "options": "Item", - "permlevel": 0, - "precision": "", - "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": "product_bundle_item", + "fieldtype": "Link", + "label": "Product Bundle Item", + "no_copy": 1, + "options": "Item", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "item_reference", + "fieldtype": "Data", + "hidden": 1, + "label": "Item Reference" } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 1, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-04-08 23:09:57.199423", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Production Plan Item", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2021-04-28 19:14:57.772123", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Plan Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/__init__.py b/erpnext/manufacturing/doctype/production_plan_item_reference/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json new file mode 100644 index 0000000000..84dee4ad28 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "creation": "2021-04-22 10:32:58.896330", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_reference", + "sales_order", + "sales_order_item", + "qty" + ], + "fields": [ + { + "fieldname": "sales_order", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Order Reference", + "options": "Sales Order" + }, + { + "fieldname": "sales_order_item", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Sales Order Item" + }, + { + "fieldname": "qty", + "fieldtype": "Data", + "in_list_view": 1, + "label": "qty" + }, + { + "fieldname": "item_reference", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Reference" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 17:03:49.707487", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Plan Item Reference", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py new file mode 100644 index 0000000000..51fbc3633b --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ProductionPlanItemReference(Document): + pass diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index a6086fb88d..3e5a72db9a 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -76,9 +76,9 @@ frappe.ui.form.on("Work Order", { frm.set_query("production_item", function() { return { query: "erpnext.controllers.queries.item_query", - filters:[ - ['is_stock_item', '=',1] - ] + filters: { + "is_stock_item": 1, + } }; }); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8507f5eb34..2600790a59 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -240,8 +240,12 @@ class WorkOrder(Document): frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) + + if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): + self.update_work_order_qty_in_combined_so() + else: + self.update_work_order_qty_in_so() - self.update_work_order_qty_in_so() self.update_reserved_qty_for_production() self.update_completed_qty_in_material_request() self.update_planned_qty() @@ -250,9 +254,13 @@ class WorkOrder(Document): def on_cancel(self): self.validate_cancel() - frappe.db.set(self,'status', 'Cancelled') - self.update_work_order_qty_in_so() + + if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): + self.update_work_order_qty_in_combined_so() + else: + self.update_work_order_qty_in_so() + self.delete_job_card() self.update_completed_qty_in_material_request() self.update_planned_qty() @@ -357,7 +365,28 @@ class WorkOrder(Document): work_order_qty = qty[0][0] if qty and qty[0][0] else 0 frappe.db.set_value('Sales Order Item', self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) + + def update_work_order_qty_in_combined_so(self): + total_bundle_qty = 1 + if self.product_bundle_item: + total_bundle_qty = frappe.db.sql(""" select sum(qty) from + `tabProduct Bundle Item` where parent = %s""", (frappe.db.escape(self.product_bundle_item)))[0][0] + if not total_bundle_qty: + # product bundle is 0 (product bundle allows 0 qty for items) + total_bundle_qty = 1 + + prod_plan = frappe.get_doc('Production Plan', self.production_plan) + item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item') + + for plan_reference in prod_plan.prod_plan_references: + work_order_qty = 0.0 + if plan_reference.item_reference == item_reference: + if self.docstatus == 1: + work_order_qty = flt(plan_reference.qty) / total_bundle_qty + frappe.db.set_value('Sales Order Item', + plan_reference.sales_order_item, 'work_order_qty', work_order_qty) + def update_completed_qty_in_material_request(self): if self.material_request: frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1e8ce3c658..93689a0ef3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -779,5 +779,7 @@ 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 +execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.set_training_event_attendance +erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold diff --git a/erpnext/patches/v12_0/purchase_receipt_status.py b/erpnext/patches/v12_0/purchase_receipt_status.py index 1a99b3163b..459221e769 100644 --- a/erpnext/patches/v12_0/purchase_receipt_status.py +++ b/erpnext/patches/v12_0/purchase_receipt_status.py @@ -19,6 +19,9 @@ def execute(): logger.info("purchase_receipt_status: begin patch, PR count: {}" .format(len(affected_purchase_receipts))) + frappe.reload_doc("stock", "doctype", "Purchase Receipt") + frappe.reload_doc("stock", "doctype", "Purchase Receipt Item") + for pr in affected_purchase_receipts: pr_name = pr[0] diff --git a/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py b/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py new file mode 100644 index 0000000000..48325fc2d4 --- /dev/null +++ b/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists('DocType', 'Issue'): + frappe.reload_doc("support", "doctype", "issue") + rename_status() + +def rename_status(): + frappe.db.sql(""" + UPDATE + `tabIssue` + SET + status = 'On Hold' + WHERE + status = 'Hold' + """) \ No newline at end of file diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 7528bf7a7f..b80b32061f 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -15,7 +15,13 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_ from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans +test_dependencies = ['Holiday List'] + class TestPayrollEntry(unittest.TestCase): + @classmethod + def setUpClass(cls): + frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List') + def setUp(self): for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry", "Salary Structure", "Salary Structure Assignment", "Payroll Employee Detail", "Additional Salary"]: diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index afdf081ac8..877503b41c 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -115,10 +115,23 @@ class SalarySlip(TransactionBase): status = "Cancelled" return status - def validate_dates(self): + def validate_dates(self, joining_date=None, relieving_date=None): if date_diff(self.end_date, self.start_date) < 0: frappe.throw(_("To date cannot be before From date")) + if not joining_date: + joining_date, relieving_date = frappe.get_cached_value( + "Employee", + self.employee, + ("date_of_joining", "relieving_date") + ) + + if date_diff(self.end_date, joining_date) < 0: + frappe.throw(_("Cannot create Salary Slip for Employee joining after Payroll Period")) + + if relieving_date and date_diff(relieving_date, self.start_date) < 0: + frappe.throw(_("Cannot create Salary Slip for Employee who has left before Payroll Period")) + def is_rounding_total_disabled(self): return cint(frappe.db.get_single_value("Payroll Settings", "disable_rounded_total")) @@ -154,9 +167,14 @@ class SalarySlip(TransactionBase): if not self.salary_slip_based_on_timesheet: self.get_date_details() - self.validate_dates() - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + + joining_date, relieving_date = frappe.get_cached_value( + "Employee", + self.employee, + ("date_of_joining", "relieving_date") + ) + + self.validate_dates(joining_date, relieving_date) #getin leave details self.get_working_days_details(joining_date, relieving_date) @@ -492,11 +510,39 @@ class SalarySlip(TransactionBase): def get_data_for_eval(self): '''Returns data for evaluating formula''' data = frappe._dict() + employee = frappe.get_doc("Employee", self.employee).as_dict() - data.update(frappe.get_doc("Salary Structure Assignment", - {"employee": self.employee, "salary_structure": self.salary_structure}).as_dict()) + start_date = getdate(self.start_date) + date_to_validate = ( + employee.date_of_joining + if employee.date_of_joining > start_date + else start_date + ) - data.update(frappe.get_doc("Employee", self.employee).as_dict()) + salary_structure_assignment = frappe.get_value( + "Salary Structure Assignment", + { + "employee": self.employee, + "salary_structure": self.salary_structure, + "from_date": ("<=", date_to_validate), + "docstatus": 1, + }, + "*", + order_by="from_date desc", + as_dict=True, + ) + + if not salary_structure_assignment: + frappe.throw( + _("Please assign a Salary Structure for Employee {0} " + "applicable from or before {1} first").format( + frappe.bold(self.employee_name), + frappe.bold(formatdate(date_to_validate)), + ) + ) + + data.update(salary_structure_assignment) + data.update(employee) data.update(self.as_dict()) # set values for components diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 01e4170d31..9e7db977ab 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -8,7 +8,6 @@ import erpnext import calendar import random from erpnext.accounts.utils import get_fiscal_year -from frappe.utils.make_random import get_random from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details @@ -155,12 +154,14 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.gross_pay, 78000) def test_payment_days(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import create_salary_structure_assignment + no_of_days = self.get_no_of_days() # Holidays not included in working days frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) # set joinng date in the same month - make_employee("test_payment_days@salary.com") + employee = make_employee("test_payment_days@salary.com") if getdate(nowdate()).day >= 15: relieving_date = getdate(add_days(nowdate(),-10)) date_of_joining = getdate(add_days(nowdate(),-10)) @@ -174,25 +175,30 @@ class TestSalarySlip(unittest.TestCase): date_of_joining = getdate(nowdate()) relieving_date = getdate(nowdate()) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", date_of_joining) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", None) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Active") + frappe.db.set_value("Employee", employee, { + "date_of_joining": date_of_joining, + "relieving_date": None, + "status": "Active" + }) - ss = make_employee_salary_slip("test_payment_days@salary.com", "Monthly", "Test Payment Days") + salary_structure = "Test Payment Days" + ss = make_employee_salary_slip("test_payment_days@salary.com", "Monthly", salary_structure) self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, (no_of_days[0] - getdate(date_of_joining).day + 1)) # set relieving date in the same month - frappe.db.set_value("Employee",frappe.get_value("Employee", - {"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", (add_days(nowdate(),-60))) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", relieving_date) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Left") + frappe.db.set_value("Employee", employee, { + "date_of_joining": add_days(nowdate(),-60), + "relieving_date": relieving_date, + "status": "Left" + }) + + if date_of_joining.day > 1: + self.assertRaises(frappe.ValidationError, ss.save) + + create_salary_structure_assignment(employee, salary_structure) + ss.reload() ss.save() self.assertEqual(ss.total_working_days, no_of_days[0]) @@ -285,6 +291,7 @@ class TestSalarySlip(unittest.TestCase): def test_multi_currency_salary_slip(self): from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + applicant = make_employee("test_multi_currency_salary_slip@salary.com", company="_Test Company") frappe.db.sql("""delete from `tabSalary Structure` where name='Test Multi Currency Salary Slip'""") salary_structure = make_salary_structure("Test Multi Currency Salary Slip", "Monthly", employee=applicant, company="_Test Company", currency='USD') @@ -325,7 +332,8 @@ class TestSalarySlip(unittest.TestCase): def test_component_wise_year_to_date_computation(self): from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - applicant = make_employee("test_ytd@salary.com", company="_Test Company") + employee_name = "test_component_wise_ytd@salary.com" + applicant = make_employee(employee_name, company="_Test Company") payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") @@ -336,13 +344,13 @@ class TestSalarySlip(unittest.TestCase): "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) # clear salary slip for this employee - frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") + frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = '%s'" % employee_name) create_salary_slips_for_payroll_period(applicant, salary_structure.name, payroll_period, deduct_random=False, num=3) salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name": - "test_ytd@salary.com"}, order_by = "posting_date") + employee_name}, order_by="posting_date") year_to_date = dict() for slip in salary_slips: @@ -380,10 +388,10 @@ class TestSalarySlip(unittest.TestCase): from erpnext.payroll.doctype.salary_structure.test_salary_structure import \ make_salary_structure, create_salary_structure_assignment + salary_structure = make_salary_structure("Stucture to test tax", "Monthly", - other_details={"max_benefits": 100000}, test_tax=True) - create_salary_structure_assignment(employee, salary_structure.name, - payroll_period.start_date) + other_details={"max_benefits": 100000}, test_tax=True, + employee=employee, payroll_period=payroll_period) # create salary slip for whole period deducting tax only on last period # to find the total tax amount paid @@ -469,6 +477,7 @@ class TestSalarySlip(unittest.TestCase): def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index 36387f23df..dce6b7aa3d 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -6,7 +6,7 @@ import frappe import unittest import erpnext from frappe.utils.make_random import get_random -from frappe.utils import nowdate, add_days, add_years, getdate, add_months +from frappe.utils import nowdate, add_years, get_first_day, date_diff from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_earning_salary_component,\ make_deduction_salary_component, make_employee_salary_slip, create_tax_slab @@ -113,8 +113,9 @@ class TestSalaryStructure(unittest.TestCase): sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency='USD') self.assertEqual(sal_struct.currency, 'USD') -def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, - test_tax=False, company=None, currency=erpnext.get_default_currency(), payroll_period=None): +def make_salary_structure(salary_structure, payroll_frequency, employee=None, + from_date=None, dont_submit=False, other_details=None,test_tax=False, + company=None, currency=erpnext.get_default_currency(), payroll_period=None): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) @@ -139,10 +140,23 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do else: salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure) + filters = {'employee':employee, 'docstatus': 1} + if not from_date and payroll_period: + from_date = payroll_period.start_date + + if from_date: + filters['from_date'] = from_date + if employee and not frappe.db.get_value("Salary Structure Assignment", - {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: - create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency, - payroll_period=payroll_period) + filters) and salary_structure_doc.docstatus==1: + create_salary_structure_assignment( + employee, + salary_structure, + from_date=from_date, + company=company, + currency=currency, + payroll_period=payroll_period + ) return salary_structure_doc @@ -165,12 +179,13 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.base = 50000 salary_structure_assignment.variable = 5000 - if getdate(nowdate()).day == 1: - date = from_date or nowdate() - else: - date = from_date or add_days(nowdate(), -1) + if not from_date: + from_date = get_first_day(nowdate()) + joining_date = frappe.get_cached_value("Employee", employee, "date_of_joining") + if date_diff(joining_date, from_date) > 0: + from_date = joining_date - salary_structure_assignment.from_date = date + salary_structure_assignment.from_date = from_date salary_structure_assignment.salary_structure = salary_structure salary_structure_assignment.currency = currency salary_structure_assignment.payroll_payable_account = get_payable_account(company) @@ -183,4 +198,4 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non def get_payable_account(company=None): if not company: company = erpnext.get_default_company() - return frappe.db.get_value("Company", company, "default_payroll_payable_account") \ No newline at end of file + return frappe.db.get_value("Company", company, "default_payroll_payable_account") diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ad1976d2d2..982b1fe1eb 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -261,11 +261,19 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) { return; } - var me = this; - var inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype) + + const me = this; + if (!this.frm.is_new() && this.frm.doc.docstatus === 0) { + this.frm.add_custom_button(__("Quality Inspection(s)"), () => { + me.make_quality_inspection(); + }, __("Create")); + this.frm.page.set_inner_btn_group_as_primary(__('Create')); + } + + const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype) ? "Incoming" : "Outgoing"; - var quality_inspection_field = this.frm.get_docfield("items", "quality_inspection"); + let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection"); quality_inspection_field.get_route_options_for_new_doc = function(row) { if(me.frm.is_new()) return; return { @@ -280,7 +288,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } this.frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; + let d = locals[cdt][cdn]; return { filters: { docstatus: 1, @@ -1949,6 +1957,130 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }); }, + make_quality_inspection: function () { + let data = []; + const fields = [ + { + label: "Items", + fieldtype: "Table", + fieldname: "items", + cannot_add_rows: true, + in_place_edit: true, + data: data, + get_data: () => { + return data; + }, + fields: [ + { + fieldtype: "Data", + fieldname: "docname", + hidden: true + }, + { + fieldtype: "Read Only", + fieldname: "item_code", + label: __("Item Code"), + in_list_view: true + }, + { + fieldtype: "Read Only", + fieldname: "item_name", + label: __("Item Name"), + in_list_view: true + }, + { + fieldtype: "Float", + fieldname: "qty", + label: __("Accepted Quantity"), + in_list_view: true, + read_only: true + }, + { + fieldtype: "Float", + fieldname: "sample_size", + label: __("Sample Size"), + reqd: true, + in_list_view: true + }, + { + fieldtype: "Data", + fieldname: "description", + label: __("Description"), + hidden: true + }, + { + fieldtype: "Data", + fieldname: "serial_no", + label: __("Serial No"), + hidden: true + }, + { + fieldtype: "Data", + fieldname: "batch_no", + label: __("Batch No"), + hidden: true + } + ] + } + ]; + + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Select Items for Quality Inspection"), + fields: fields, + primary_action: function () { + const data = dialog.get_values(); + frappe.call({ + method: "erpnext.controllers.stock_controller.make_quality_inspections", + args: { + doctype: me.frm.doc.doctype, + docname: me.frm.doc.name, + items: data.items + }, + freeze: true, + callback: function (r) { + if (r.message.length > 0) { + if (r.message.length === 1) { + frappe.set_route("Form", "Quality Inspection", r.message[0]); + } else { + frappe.route_options = { + "reference_type": me.frm.doc.doctype, + "reference_name": me.frm.doc.name + }; + frappe.set_route("List", "Quality Inspection"); + } + } + dialog.hide(); + } + }); + }, + primary_action_label: __("Create") + }); + + this.frm.doc.items.forEach(item => { + if (!item.quality_inspection) { + let dialog_items = dialog.fields_dict.items; + dialog_items.df.data.push({ + "docname": item.name, + "item_code": item.item_code, + "item_name": item.item_name, + "qty": item.qty, + "description": item.description, + "serial_no": item.serial_no, + "batch_no": item.batch_no + }); + dialog_items.grid.refresh(); + } + }); + + data = dialog.fields_dict.items.df.data; + if (!data.length) { + frappe.msgprint(__("All items in this document already have a linked Quality Inspection.")); + } else { + dialog.show(); + } + }, + get_method_for_payment: function(){ var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){ diff --git a/erpnext/public/js/education/lms/quiz.js b/erpnext/public/js/education/lms/quiz.js index 32fa4ab1ec..66160a7610 100644 --- a/erpnext/public/js/education/lms/quiz.js +++ b/erpnext/public/js/education/lms/quiz.js @@ -20,10 +20,8 @@ class Quiz { } make(data) { - if (data.duration) { - const timer_display = document.createElement("div"); - timer_display.classList.add("lms-timer", "float-right", "font-weight-bold"); - document.getElementsByClassName("lms-title")[0].appendChild(timer_display); + if (data.is_time_bound) { + $(".lms-timer").removeClass("hide"); if (!data.activity || (data.activity && !data.activity.is_complete)) { this.initialiseTimer(data.duration); this.is_time_bound = true; @@ -118,7 +116,7 @@ class Quiz { quiz_response: this.get_selected(), course: this.course, program: this.program, - time_taken: this.is_time_bound ? this.time_taken : "" + time_taken: this.is_time_bound ? this.time_taken : 0 }).then(res => { this.submit_btn.remove() if (!res.message) { @@ -237,4 +235,4 @@ class Question { this.options = option_list this.wrapper.appendChild(options_wrapper) } -} \ No newline at end of file +} diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 159a8a47cd..9402cf9ea4 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -1,4 +1,3 @@ -@import "frappe/public/scss/desk/variables"; @import "frappe/public/scss/common/mixins"; body.product-page { @@ -74,15 +73,6 @@ body.product-page { } } - // .card-body { - // text-align: center; - // } - - // .featured-item { - // .card-body { - // text-align: left; - // } - // } .card-img-container { height: 210px; width: 100%; @@ -217,12 +207,12 @@ body.product-page { border-color: var(--table-border-color) !important; padding: 15px; - @include media-breakpoint-between(xs, md) { + @media (max-width: var(--md-width)) { height: 300px; width: 300px; } - @include media-breakpoint-up(lg) { + @media (min-width: var(--lg-width)) { height: 350px; width: 350px; } @@ -233,11 +223,12 @@ body.product-page { } .item-slideshow { - @include media-breakpoint-between(xs, md) { + + @media (max-width: var(--md-width)) { max-height: 320px; } - @include media-breakpoint-up(lg) { + @media (min-width: var(--lg-width)) { max-height: 430px; } @@ -254,7 +245,7 @@ body.product-page { cursor: pointer; &:hover, &.active { - border-color: $primary; + border-color: var(--primary); } } @@ -316,12 +307,9 @@ body.product-page { } .item-group-slideshow { - .item-group-description { - // max-width: 900px; - } .carousel-inner.rounded-carousel { - border-radius: $card-border-radius; + border-radius: var(--card-border-radius); } } diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss index 56b717c424..f4325c03f5 100644 --- a/erpnext/public/scss/website.scss +++ b/erpnext/public/scss/website.scss @@ -1,4 +1,3 @@ -@import "frappe/public/scss/website/variables"; .filter-options { max-height: 300px; @@ -14,7 +13,7 @@ } &.active { - border-color: $primary; + border-color: var(--primary); .check { display: inline-flex; @@ -25,7 +24,7 @@ .check { display: inline-flex; padding: 0.25rem; - background: $primary; + background: var(--primary); color: white; border-radius: 50%; font-size: 12px; @@ -38,12 +37,12 @@ } .result { - border-bottom: 1px solid $border-color; + border-bottom: 1px solid var(--border-color); } .transaction-list-item { padding: 1rem 0; - border-top: 1px solid $border-color; + border-top: 1px solid var(--border-color); position: relative; a.transaction-item-link { diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 3ddcc58867..641520437f 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -310,7 +310,7 @@ class GSTR3BReport(Document): self.report_dict['sup_details']['osup_det']['txval'] += taxable_value gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category') - place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply', '00-Other Territory') + place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory' if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \ self.gst_details.get("gst_state") != place_of_supply.split("-")[1]: diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 95b4e16afd..5a53d49399 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -194,7 +194,7 @@ def get_item_list(invoice): 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 = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' + item.is_service_item = 'Y' if item.gst_hsn_code[:2] == "99" else 'N' item.serial_no = "" item = update_item_taxes(invoice, item) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index ea61502099..a4466e78f2 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -500,7 +500,7 @@ def download_ewb_json(): if not isinstance(docname, list): # removes characters not allowed in a filename (https://stackoverflow.com/a/38766141/4767738) - filename_prefix = re.sub('[^\w_.)( -]', '', docname) + filename_prefix = re.sub(r'[^\w_.)( -]', '', docname) frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(filename_prefix, frappe.utils.random_string(5)) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index b7c096248f..80e2d725a2 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -147,6 +147,13 @@ class Gstr1Report(object): def get_invoice_data(self): self.invoices = frappe._dict() conditions = self.get_conditions() + + company_gstins = get_company_gstin_number(self.filters.get('company'), all_gstins=True) + + self.filters.update({ + 'company_gstins': company_gstins + }) + invoice_data = frappe.db.sql(""" select {select_columns} @@ -193,6 +200,9 @@ class Gstr1Report(object): elif self.filters.get("type_of_business") == "EXPORT": conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ + + conditions += " AND billing_address_gstin NOT IN %(company_gstins)s" + return conditions def get_invoice_items(self): @@ -810,7 +820,8 @@ def get_rate_and_tax_details(row, gstin): return {"num": int(num), "itm_det": itm_det} -def get_company_gstin_number(company, address=None): +def get_company_gstin_number(company, address=None, all_gstins=False): + gstin = '' if address: gstin = frappe.db.get_value("Address", address, "gstin") @@ -822,9 +833,9 @@ def get_company_gstin_number(company, address=None): ["Dynamic Link", "parenttype", "=", "Address"], ] gstin = frappe.get_all("Address", filters=filters, pluck="gstin") - if gstin: - gstin[0] - + if gstin and not all_gstins: + gstin = gstin[0] + if not gstin: address = frappe.bold(address) if address else "" frappe.throw(_("Please set valid GSTIN No. in Company Address {} for company {}").format( diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 750a1a6071..7742f24385 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -8,39 +8,52 @@ from frappe.utils import cint from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability -from six import string_types +def search_by_term(search_term, warehouse, price_list): + result = search_for_serial_or_batch_or_barcode_number(search_term) + + item_code = result.get("item_code") or search_term + serial_no = result.get("serial_no") or "" + batch_no = result.get("batch_no") or "" + barcode = result.get("barcode") or "" + + if result: + item_info = frappe.db.get_value("Item", item_code, + ["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"], + as_dict=1) + + item_stock_qty = get_stock_availability(item_code, warehouse) + price_list_rate, currency = frappe.db.get_value('Item Price', { + 'price_list': price_list, + 'item_code': item_code + }, ["price_list_rate", "currency"]) + + item_info.update({ + 'serial_no': serial_no, + 'batch_no': batch_no, + 'barcode': barcode, + 'price_list_rate': price_list_rate, + 'currency': currency, + 'actual_qty': item_stock_qty + }) + + return {'items': [item_info]} @frappe.whitelist() -def get_items(start, page_length, price_list, item_group, pos_profile, search_value=""): - data = dict() +def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""): + warehouse, hide_unavailable_items = frappe.db.get_value( + 'POS Profile', pos_profile, ['warehouse', 'hide_unavailable_items']) + result = [] - allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') - warehouse, hide_unavailable_items = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'hide_unavailable_items']) + if search_term: + result = search_by_term(search_term, warehouse, price_list) + if result: + return result if not frappe.db.exists('Item Group', item_group): item_group = get_root_of('Item Group') - if search_value: - data = search_serial_or_batch_or_barcode_number(search_value) - - item_code = data.get("item_code") if data.get("item_code") else search_value - serial_no = data.get("serial_no") if data.get("serial_no") else "" - batch_no = data.get("batch_no") if data.get("batch_no") else "" - barcode = data.get("barcode") if data.get("barcode") else "" - - if data: - item_info = frappe.db.get_value( - "Item", data.get("item_code"), - ["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"] - , as_dict=1) - item_info.setdefault('serial_no', serial_no) - item_info.setdefault('batch_no', batch_no) - item_info.setdefault('barcode', barcode) - - return { 'items': [item_info] } - - condition = get_conditions(item_code, serial_no, batch_no, barcode) + condition = get_conditions(search_term) condition += get_item_group_condition(pos_profile) lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt']) @@ -62,7 +75,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va `tabItem` item {bin_join_selection} WHERE item.disabled = 0 - AND item.is_stock_item = 1 AND item.has_variants = 0 AND item.is_sales_item = 1 AND item.is_fixed_asset = 0 @@ -84,6 +96,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va ), {'warehouse': warehouse}, as_dict=1) if items_data: + items_data = filter_service_items(items_data) items = [d.item_code for d in items_data] item_prices_data = frappe.get_all("Item Price", fields = ["item_code", "price_list_rate", "currency"], @@ -96,10 +109,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va for item in items_data: item_code = item.item_code item_price = item_prices.get(item_code) or {} - if allow_negative_stock: - item_stock_qty = frappe.db.sql("""select ifnull(sum(actual_qty), 0) from `tabBin` where item_code = %s""", item_code)[0][0] - else: - item_stock_qty = get_stock_availability(item_code, warehouse) + item_stock_qty = get_stock_availability(item_code, warehouse) row = {} row.update(item) @@ -110,14 +120,10 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va }) result.append(row) - res = { - 'items': result - } - - return res + return {'items': result} @frappe.whitelist() -def search_serial_or_batch_or_barcode_number(search_value): +def search_for_serial_or_batch_or_barcode_number(search_value): # search barcode no barcode_data = frappe.db.get_value('Item Barcode', {'barcode': search_value}, ['barcode', 'parent as item_code'], as_dict=True) if barcode_data: @@ -135,27 +141,29 @@ def search_serial_or_batch_or_barcode_number(search_value): return {} -def get_conditions(item_code, serial_no, batch_no, barcode): - if serial_no or batch_no or barcode: - return "item.name = {0}".format(frappe.db.escape(item_code)) +def filter_service_items(items): + for item in items: + if not item['is_stock_item']: + if not frappe.db.exists('Product Bundle', item['item_code']): + items.remove(item) + + return items - return make_condition(item_code) - -def make_condition(item_code): +def get_conditions(search_term): condition = "(" - condition += """item.name like {item_code} - or item.item_name like {item_code}""".format(item_code = frappe.db.escape('%' + item_code + '%')) - condition += add_search_fields_condition(item_code) + condition += """item.name like {search_term} + or item.item_name like {search_term}""".format(search_term=frappe.db.escape('%' + search_term + '%')) + condition += add_search_fields_condition(search_term) condition += ")" return condition -def add_search_fields_condition(item_code): +def add_search_fields_condition(search_term): condition = '' search_fields = frappe.get_all('POS Search Fields', fields = ['fieldname']) if search_fields: for field in search_fields: - condition += " or item.{0} like {1}".format(field['fieldname'], frappe.db.escape('%' + item_code + '%')) + condition += " or item.`{0}` like {1}".format(field['fieldname'], frappe.db.escape('%' + search_term + '%')) return condition def get_item_group_condition(pos_profile): diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 4f4f1b2240..ae3f9e3c9d 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -241,10 +241,8 @@ erpnext.PointOfSale.Controller = class { events: { get_frm: () => this.frm, - cart_item_clicked: (item_code, batch_no, uom) => { - const search_field = batch_no ? 'batch_no' : 'item_code'; - const search_value = batch_no || item_code; - const item_row = this.frm.doc.items.find(i => i[search_field] === search_value && i.uom === uom); + cart_item_clicked: (item_code, batch_no, uom, rate) => { + const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); this.item_details.toggle_item_details_section(item_row); }, @@ -275,18 +273,25 @@ erpnext.PointOfSale.Controller = class { this.cart.toggle_numpad(minimize); }, - form_updated: async (cdt, cdn, fieldname, value) => { + form_updated: (cdt, cdn, fieldname, value) => { const item_row = frappe.model.get_doc(cdt, cdn); if (item_row && item_row[fieldname] != value) { - const { item_code, batch_no, uom } = this.item_details.current_item; + const { item_code, batch_no, uom, rate } = this.item_details.current_item; const event = { field: fieldname, value, - item: { item_code, batch_no, uom } + item: { item_code, batch_no, uom, rate } } return this.on_cart_update(event) } + + return Promise.resolve(); + }, + + highlight_cart_item: (item) => { + const cart_item = this.cart.get_cart_item(item); + this.cart.toggle_item_highlight(cart_item); }, item_field_focused: (fieldname) => { @@ -501,8 +506,8 @@ erpnext.PointOfSale.Controller = class { let item_row = undefined; try { let { field, value, item } = args; - const { item_code, batch_no, serial_no, uom } = item; - item_row = this.get_item_from_frm(item_code, batch_no, uom); + const { item_code, batch_no, serial_no, uom, rate } = item; + item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); const item_selected_from_selector = field === 'qty' && value === "+1" @@ -535,7 +540,7 @@ erpnext.PointOfSale.Controller = class { item_selected_from_selector && (value = flt(value)) - const args = { item_code, batch_no, [field]: value }; + const args = { item_code, batch_no, rate, [field]: value }; if (serial_no) { await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); @@ -550,9 +555,11 @@ erpnext.PointOfSale.Controller = class { await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse); await this.trigger_new_item_events(item_row); - - this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row); + this.update_cart_html(item_row); + + this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row); + this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row); } } catch (error) { @@ -563,12 +570,13 @@ erpnext.PointOfSale.Controller = class { } } - get_item_from_frm(item_code, batch_no, uom) { + get_item_from_frm(item_code, batch_no, uom, rate) { const has_batch_no = batch_no; return this.frm.doc.items.find( i => i.item_code === item_code && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) && (i.uom === uom) + && (i.rate == rate) ); } diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 11a63b3d4a..f5019f5083 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -184,7 +184,8 @@ erpnext.PointOfSale.ItemCart = class { const item_code = unescape($cart_item.attr('data-item-code')); const batch_no = unescape($cart_item.attr('data-batch-no')); const uom = unescape($cart_item.attr('data-uom')); - me.events.cart_item_clicked(item_code, batch_no, uom); + const rate = unescape($cart_item.attr('data-rate')); + me.events.cart_item_clicked(item_code, batch_no, uom, rate); this.numpad_value = ''; }); @@ -520,28 +521,34 @@ erpnext.PointOfSale.ItemCart = class { } } - get_cart_item({ item_code, batch_no, uom }) { + get_cart_item({ item_code, batch_no, uom, rate }) { const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; const item_code_attr = `[data-item-code="${escape(item_code)}"]`; const uom_attr = `[data-uom="${escape(uom)}"]`; + const rate_attr = `[data-rate="${escape(rate)}"]`; const item_selector = batch_no ? - `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; + `.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`; return this.$cart_items_wrapper.find(item_selector); } + get_item_from_frm(item) { + const doc = this.events.get_frm().doc; + const { item_code, batch_no, uom, rate } = item; + const search_field = batch_no ? 'batch_no' : 'item_code'; + const search_value = batch_no || item_code; + + return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate); + } + update_item_html(item, remove_item) { const $item = this.get_cart_item(item); if (remove_item) { $item && $item.next().remove() && $item.remove(); } else { - const { item_code, batch_no, uom } = item; - const search_field = batch_no ? 'batch_no' : 'item_code'; - const search_value = batch_no || item_code; - const item_row = this.events.get_frm().doc.items.find(i => i[search_field] === search_value && i.uom === uom); - + const item_row = this.get_item_from_frm(item); this.render_cart_item(item_row, $item); } @@ -559,7 +566,7 @@ erpnext.PointOfSale.ItemCart = class { this.$cart_items_wrapper.append( `
+ data-batch-no="${escape(item_data.batch_no || '')}" data-rate="${escape(item_data.rate)}">
` ) @@ -636,13 +643,23 @@ erpnext.PointOfSale.ItemCart = class { function get_item_image_html() { const { image, item_name } = item_data; if (image) { - return `
${image}
`; + return ` +
+ ${frappe.get_abbr(item_name)} +
`; } else { return `
${frappe.get_abbr(item_name)}
`; } } } + handle_broken_image($img) { + const item_abbr = $($img).attr('alt'); + $($img).parent().replaceWith(`
${item_abbr}
`); + } + scroll_to_item($item) { if ($item.length === 0) return; const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop(); diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 32a4556766..5e09df8efe 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -54,13 +54,24 @@ erpnext.PointOfSale.ItemDetails = class { this.$dicount_section = this.$component.find('.discount-section'); } - toggle_item_details_section(item) { - const { item_code, batch_no, uom } = this.current_item; + has_item_has_changed(item) { + const { item_code, batch_no, uom, rate } = this.current_item; const item_code_is_same = item && item_code === item.item_code; const batch_is_same = item && batch_no == item.batch_no; const uom_is_same = item && uom === item.uom; + const rate_is_same = item && rate === item.rate; + + if (!item) + return false; - this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true; + if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same) + return false; + + return true; + } + + toggle_item_details_section(item) { + this.item_has_changed = this.has_item_has_changed(item); this.events.toggle_item_selector(this.item_has_changed); this.toggle_component(this.item_has_changed); @@ -72,11 +83,12 @@ erpnext.PointOfSale.ItemDetails = class { this.item_row = item; this.currency = this.events.get_frm().doc.currency; - this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom }; + this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate }; this.render_dom(item); this.render_discount_dom(item); this.render_form(item); + this.events.highlight_cart_item(item); } else { this.validate_serial_batch_item(); this.current_item = {}; @@ -121,13 +133,24 @@ erpnext.PointOfSale.ItemDetails = class { this.$item_description.html(get_description_html()); this.$item_price.html(format_currency(price_list_rate, this.currency)); if (image) { - this.$item_image.html(`${image}`); + this.$item_image.html( + `${frappe.get_abbr(item_name)}` + ); } else { this.$item_image.html(`
${frappe.get_abbr(item_name)}
`); } } + handle_broken_image($img) { + const item_abbr = $($img).attr('alt'); + $($img).replaceWith(`
${item_abbr}
`); + } + render_discount_dom(item) { if (item.discount_percentage) { this.$dicount_section.html( @@ -198,12 +221,14 @@ erpnext.PointOfSale.ItemDetails = class { if (this.allow_rate_change) { this.rate_control.df.onchange = function() { if (this.value || flt(this.value) === 0) { + me.events.set_value_in_current_cart_item('rate', this.value); me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => { const item_row = frappe.get_doc(me.doctype, me.name); const doc = me.events.get_frm().doc; me.$item_price.html(format_currency(item_row.rate, doc.currency)); me.render_discount_dom(item_row); }); + me.current_item.rate = this.value; } }; } else { @@ -292,11 +317,7 @@ erpnext.PointOfSale.ItemDetails = class { frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { const field_control = this[`${fieldname}_control`]; - const { item_code, batch_no, uom } = this.current_item; - const item_code_is_same = item_code === item_row.item_code; - const batch_is_same = batch_no == item_row.batch_no; - const uom_is_same = uom === item_row.uom; - const item_is_same = item_code_is_same && batch_is_same && uom_is_same ? true : false; + const item_is_same = !this.has_item_has_changed(item_row); if (item_is_same && field_control && field_control.get_value() !== value) { field_control.set_value(value); diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index b8a82a9eda..64c529ee4a 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -51,7 +51,7 @@ erpnext.PointOfSale.ItemSelector = class { }); } - get_items({start = 0, page_length = 40, search_value=''}) { + get_items({start = 0, page_length = 40, search_term=''}) { const doc = this.events.get_frm().doc; const price_list = (doc && doc.selling_price_list) || this.price_list; let { item_group, pos_profile } = this; @@ -61,7 +61,7 @@ erpnext.PointOfSale.ItemSelector = class { return frappe.call({ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items", freeze: true, - args: { start, page_length, price_list, item_group, search_value, pos_profile }, + args: { start, page_length, price_list, item_group, search_term, pos_profile }, }); } @@ -78,8 +78,9 @@ erpnext.PointOfSale.ItemSelector = class { get_item_html(item) { const me = this; // eslint-disable-next-line no-unused-vars - const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; + const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item; const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"; + const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0; let qty_to_display = actual_qty; @@ -94,7 +95,11 @@ erpnext.PointOfSale.ItemSelector = class { ${qty_to_display}
- ${frappe.get_abbr(item.item_name)} + ${frappe.get_abbr(item.item_name)}
`; } else { return `
@@ -108,6 +113,7 @@ erpnext.PointOfSale.ItemSelector = class { `
${get_item_image_html()} @@ -116,12 +122,17 @@ erpnext.PointOfSale.ItemSelector = class {
${frappe.ellipsis(item.item_name, 18)}
-
${format_currency(item.price_list_rate, item.currency, 0) || 0}
+
${format_currency(price_list_rate, item.currency, precision) || 0}
` ); } + handle_broken_image($img) { + const item_abbr = $($img).attr('alt'); + $($img).parent().replaceWith(`
${item_abbr}
`); + } + make_search_bar() { const me = this; const doc = me.events.get_frm().doc; @@ -213,13 +224,15 @@ erpnext.PointOfSale.ItemSelector = class { let batch_no = unescape($item.attr('data-batch-no')); let serial_no = unescape($item.attr('data-serial-no')); let uom = unescape($item.attr('data-uom')); + let rate = unescape($item.attr('data-rate')); // escape(undefined) returns "undefined" then unescape returns "undefined" batch_no = batch_no === "undefined" ? undefined : batch_no; serial_no = serial_no === "undefined" ? undefined : serial_no; uom = uom === "undefined" ? undefined : uom; + rate = rate === "undefined" ? undefined : rate; - me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); + me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }}); me.set_search_value(''); }); @@ -290,7 +303,7 @@ erpnext.PointOfSale.ItemSelector = class { } } - this.get_items({ search_value: search_term }) + this.get_items({ search_term }) .then(({ message }) => { // eslint-disable-next-line no-unused-vars const { items, serial_no, batch_no, barcode } = message; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 600f160490..156fb777fe 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -171,7 +171,7 @@ erpnext.PointOfSale.Payment = class { this.setup_listener_for_payments(); - this.$payment_modes.on('click', '.shortcut', () => { + this.$payment_modes.on('click', '.shortcut', function() { const value = $(this).attr('data-value'); me.selected_mode.set_value(value); }); diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 8c97322a71..340d89bdf8 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -249,7 +249,7 @@ class EmailDigest(Document): card = cache.get(cache_key) if card: - card = eval(card) + card = frappe.safe_eval(card) else: card = frappe._dict(getattr(self, "get_" + key)()) @@ -808,7 +808,6 @@ def get_incomes_expenses_for_period(account, from_date, to_date): val = balance_on_to_date - balance_before_from_date else: last_year_closing_balance = get_balance_on(account, date=fy_start_date - timedelta(days=1)) - print(fy_start_date - timedelta(days=1), last_year_closing_balance) val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date) return val @@ -837,4 +836,4 @@ def get_future_date_for_calendaer_event(frequency): elif frequency == "Monthly": to_date = add_to_date(from_date, months=1) - return from_date, to_date \ No newline at end of file + return from_date, to_date diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 38f8de7a66..ece9fb5699 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -12,10 +12,6 @@ from frappe.desk.notifications import clear_notifications class TransactionDeletionRecord(Document): def validate(self): frappe.only_for('System Manager') - company_obj = frappe.get_doc('Company', self.company) - if frappe.session.user != company_obj.owner and frappe.session.user != 'Administrator': - frappe.throw(_('Transactions can only be deleted by the creator of the Company or the Administrator.'), - frappe.PermissionError) doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in self.doctypes_to_be_ignored: if doctype.doctype_name not in doctypes_to_be_ignored_list: diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 5876488033..ec9a6d6b70 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -481,37 +481,42 @@ }, "Germany": { + "tax_categories": [ + "Umsatzsteuer", + "Vorsteuer" + ], "chart_of_accounts": { "SKR04 mit Kontonummern": { "sales_tax_templates": [ { - "title": "Umsatzsteuer 19%", + "title": "Umsatzsteuer", + "tax_category": "Umsatzsteuer", + "is_default": 1, "taxes": [ { "account_head": { "account_name": "Umsatzsteuer 19%", "account_number": "3806", "tax_rate": 19.00 - } - } - ] - }, - { - "title": "Umsatzsteuer 7%", - "taxes": [ + }, + "rate": 0.00 + }, { "account_head": { "account_name": "Umsatzsteuer 7%", "account_number": "3801", "tax_rate": 7.00 - } + }, + "rate": 0.00 } ] } ], "purchase_tax_templates": [ { - "title": "Abziehbare Vorsteuer 19%", + "title": "Vorsteuer", + "tax_category": "Vorsteuer", + "is_default": 1, "taxes": [ { "account_head": { @@ -519,20 +524,17 @@ "account_number": "1406", "root_type": "Asset", "tax_rate": 19.00 - } - } - ] - }, - { - "title": "Abziehbare Vorsteuer 7%", - "taxes": [ + }, + "rate": 0.00 + }, { "account_head": { "account_name": "Abziehbare Vorsteuer 7%", "account_number": "1401", "root_type": "Asset", "tax_rate": 7.00 - } + }, + "rate": 0.00 } ] }, @@ -559,38 +561,129 @@ } ] } - ] - }, - "SKR03 mit Kontonummern": { - "sales_tax_templates": [ + ], + "item_tax_templates": [ { "title": "Umsatzsteuer 19%", "taxes": [ { - "account_head": { + "tax_type": { "account_name": "Umsatzsteuer 19%", - "account_number": "1776", + "account_number": "3806", "tax_rate": 19.00 - } + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7%", + "account_number": "3801", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 } ] }, { "title": "Umsatzsteuer 7%", "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19%", + "account_number": "3806", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7%", + "account_number": "3801", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + } + ] + }, + { + "title": "Vorsteuer 19%", + "taxes": [ + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19%", + "account_number": "1406", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7%", + "account_number": "1401", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + } + ] + }, + { + "title": "Vorsteuer 7%", + "taxes": [ + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19%", + "account_number": "1406", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7%", + "account_number": "1401", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + } + ] + } + ] + }, + "SKR03 mit Kontonummern": { + "sales_tax_templates": [ + { + "title": "Umsatzsteuer", + "tax_category": "Umsatzsteuer", + "is_default": 1, + "taxes": [ + { + "account_head": { + "account_name": "Umsatzsteuer 19%", + "account_number": "1776", + "tax_rate": 19.00 + }, + "rate": 0.00 + }, { "account_head": { "account_name": "Umsatzsteuer 7%", "account_number": "1771", "tax_rate": 7.00 - } + }, + "rate": 0.00 } ] } ], "purchase_tax_templates": [ { - "title": "Abziehbare Vorsteuer 19%", + "title": "Vorsteuer", + "tax_category": "Vorsteuer", + "is_default": 1, "taxes": [ { "account_head": { @@ -598,20 +691,107 @@ "account_number": "1576", "root_type": "Asset", "tax_rate": 19.00 - } - } - ] - }, - { - "title": "Abziehbare Vorsteuer 7%", - "taxes": [ + }, + "rate": 0.00 + }, { "account_head": { "account_name": "Abziehbare Vorsteuer 7%", "account_number": "1571", "root_type": "Asset", "tax_rate": 7.00 - } + }, + "rate": 0.00 + } + ] + } + ], + "item_tax_templates": [ + { + "title": "Umsatzsteuer 19%", + "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19%", + "account_number": "1776", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7%", + "account_number": "1771", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + } + ] + }, + { + "title": "Umsatzsteuer 7%", + "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19%", + "account_number": "1776", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7%", + "account_number": "1771", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + } + ] + }, + { + "title": "Vorsteuer 19%", + "taxes": [ + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19%", + "account_number": "1576", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7%", + "account_number": "1571", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + } + ] + }, + { + "title": "Vorsteuer 7%", + "taxes": [ + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19%", + "account_number": "1576", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7%", + "account_number": "1571", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 } ] } @@ -620,33 +800,34 @@ "Standard with Numbers": { "sales_tax_templates": [ { - "title": "Umsatzsteuer 19%", + "title": "Umsatzsteuer", + "tax_category": "Umsatzsteuer", + "is_default": 1, "taxes": [ { "account_head": { "account_name": "Umsatzsteuer 19%", "account_number": "2301", "tax_rate": 19.00 - } - } - ] - }, - { - "title": "Umsatzsteuer 7%", - "taxes": [ + }, + "rate": 0.00 + }, { "account_head": { "account_name": "Umsatzsteuer 7%", "account_number": "2302", "tax_rate": 7.00 - } + }, + "rate": 0.00 } ] } ], "purchase_tax_templates": [ { - "title": "Abziehbare Vorsteuer 19%", + "title": "Vorsteuer", + "tax_category": "Vorsteuer", + "is_default": 1, "taxes": [ { "account_head": { @@ -654,20 +835,107 @@ "account_number": "1501", "root_type": "Asset", "tax_rate": 19.00 - } - } - ] - }, - { - "title": "Abziehbare Vorsteuer 7%", - "taxes": [ + }, + "rate": 0.00 + }, { "account_head": { "account_name": "Abziehbare Vorsteuer 7%", "account_number": "1502", "root_type": "Asset", "tax_rate": 7.00 - } + }, + "rate": 0.00 + } + ] + } + ], + "item_tax_templates": [ + { + "title": "Umsatzsteuer 19%", + "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19%", + "account_number": "2301", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7%", + "account_number": "2302", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + } + ] + }, + { + "title": "Umsatzsteuer 7%", + "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19%", + "account_number": "2301", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7%", + "account_number": "2302", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + } + ] + }, + { + "title": "Vorsteuer 19%", + "taxes": [ + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19%", + "account_number": "1501", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7%", + "account_number": "1502", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + } + ] + }, + { + "title": "Vorsteuer 7%", + "taxes": [ + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19%", + "account_number": "1501", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7%", + "account_number": "1502", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 } ] } @@ -676,13 +944,69 @@ "*": { "sales_tax_templates": [ { - "title": "Umsatzsteuer 19%", + "title": "Umsatzsteuer", + "tax_category": "Umsatzsteuer", + "is_default": 1, "taxes": [ { "account_head": { "account_name": "Umsatzsteuer 19%", "tax_rate": 19.00 - } + }, + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer 7%", + "tax_rate": 7.00 + }, + "rate": 0.00 + } + ] + } + ], + "purchase_tax_templates": [ + { + "title": "Vorsteuer 19%", + "tax_category": "Vorsteuer", + "is_default": 1, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19%", + "tax_rate": 19.00, + "root_type": "Asset" + }, + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7%", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "rate": 0.00 + } + ] + } + ], + "item_tax_templates": [ + { + "title": "Umsatzsteuer 19%", + "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19%", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7%", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 } ] }, @@ -690,36 +1014,60 @@ "title": "Umsatzsteuer 7%", "taxes": [ { - "account_head": { + "tax_type": { + "account_name": "Umsatzsteuer 19%", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { "account_name": "Umsatzsteuer 7%", "tax_rate": 7.00 - } - } - ] - } - ], - "purchase_tax_templates": [ - { - "title": "Abziehbare Vorsteuer 19%", - "taxes": [ - { - "account_head": { - "account_name": "Abziehbare Vorsteuer 19%", - "tax_rate": 19.00, - "root_type": "Asset" - } + }, + "tax_rate": 7.00 } ] }, { - "title": "Abziehbare Vorsteuer 7%", + "title": "Vorsteuer 19%", "taxes": [ { - "account_head": { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19%", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { "account_name": "Abziehbare Vorsteuer 7%", "root_type": "Asset", "tax_rate": 7.00 - } + }, + "tax_rate": 0.00 + } + ] + }, + { + "title": "Vorsteuer 7%", + "taxes": [ + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19%", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7%", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 } ] } diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 429a558c58..f4fe18e116 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -11,6 +11,9 @@ from frappe import _ def setup_taxes_and_charges(company_name: str, country: str): + if not frappe.db.exists('Company', company_name): + frappe.throw(_('Company {} does not exist yet. Taxes setup aborted.').format(company_name)) + file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'country_wise_tax.json') with open(file_path, 'r') as json_file: tax_data = json.load(json_file) @@ -23,7 +26,7 @@ def setup_taxes_and_charges(company_name: str, country: str): if 'chart_of_accounts' not in country_wise_tax: country_wise_tax = simple_to_detailed(country_wise_tax) - from_detailed_data(company_name, country_wise_tax.get('chart_of_accounts')) + from_detailed_data(company_name, country_wise_tax) def simple_to_detailed(templates): @@ -74,10 +77,16 @@ def simple_to_detailed(templates): def from_detailed_data(company_name, data): """Create Taxes and Charges Templates from detailed data.""" coa_name = frappe.db.get_value('Company', company_name, 'chart_of_accounts') - tax_templates = data.get(coa_name) or data.get('*') - sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*') - purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*') - item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*') + coa_data = data.get('chart_of_accounts', {}) + tax_templates = coa_data.get(coa_name) or coa_data.get('*', {}) + tax_categories = data.get('tax_categories') + sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*', {}) + purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*', {}) + item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*', {}) + + if tax_categories: + for tax_category in tax_categories: + make_tax_catgory(tax_category) if sales_tax_templates: for template in sales_tax_templates: @@ -106,6 +115,9 @@ def make_taxes_and_charges_template(company_name, doctype, template): 'charge_type': 'On Net Total' } + if doctype == 'Purchase Taxes and Charges Template': + tax_row_defaults['add_deduct_tax'] = 'Add' + # if account_head is a dict, search or create the account and get it's name if isinstance(account_data, dict): tax_row_defaults['description'] = '{0} @ {1}'.format(account_data.get('account_name'), account_data.get('tax_rate')) @@ -230,3 +242,14 @@ def get_or_create_tax_group(company_name, root_type): tax_group_name = tax_group_account.name return tax_group_name + + +def make_tax_catgory(tax_category): + doctype = 'Tax Category' + if isinstance(tax_category, str): + tax_category = {'title': tax_category} + + tax_category['doctype'] = doctype + if not frappe.db.exists(doctype, tax_category['title']): + doc = frappe.get_doc(tax_category) + doc.insert(ignore_permissions=True) diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/shopping_cart/test_shopping_cart.py index d857bf5f5c..ac61aebc56 100644 --- a/erpnext/shopping_cart/test_shopping_cart.py +++ b/erpnext/shopping_cart/test_shopping_cart.py @@ -7,7 +7,7 @@ import frappe from frappe.utils import nowdate, add_months from erpnext.shopping_cart.cart import _get_cart_quotation, update_cart, get_party from erpnext.tests.utils import create_test_contact_and_address - +from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule # test_dependencies = ['Payment Terms Template'] @@ -125,7 +125,7 @@ class TestShoppingCart(unittest.TestCase): tax_rule = frappe.get_test_records("Tax Rule")[0] try: frappe.get_doc(tax_rule).insert() - except frappe.DuplicateEntryError: + except (frappe.DuplicateEntryError, ConflictingTaxRule): pass def create_quotation(self): diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index dbac79465e..dd815404fa 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1,8 +1,6 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals - import itertools import json import erpnext @@ -12,7 +10,7 @@ from erpnext.controllers.item_variant import (ItemVariantExistsError, copy_attributes_to_variant, get_variant, make_variant_item_code, validate_item_variant_attributes) from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) from frappe import _, msgprint -from frappe.utils import (cint, cstr, flt, formatdate, get_timestamp, getdate, +from frappe.utils import (cint, cstr, flt, formatdate, getdate, now_datetime, random_string, strip, get_link_to_form, nowtime) from frappe.utils.html_utils import clean_html from frappe.website.doctype.website_slideshow.website_slideshow import \ @@ -21,8 +19,6 @@ from frappe.website.doctype.website_slideshow.website_slideshow import \ from frappe.website.render import clear_cache from frappe.website.website_generator import WebsiteGenerator -from six import iteritems - class DuplicateReorderRows(frappe.ValidationError): pass @@ -76,8 +72,6 @@ class Item(WebsiteGenerator): if not self.description: self.description = self.item_name - # if self.is_sales_item and not self.get('is_item_from_hub'): - # self.publish_in_hub = 1 def after_insert(self): '''set opening stock and item price''' @@ -129,7 +123,7 @@ class Item(WebsiteGenerator): self.cant_change() self.update_show_in_website() - if not self.get("__islocal"): + if not self.is_new(): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") self.old_website_item_groups = frappe.db.sql_list("""select item_group from `tabWebsite Item Group` @@ -203,7 +197,7 @@ class Item(WebsiteGenerator): def make_route(self): if not self.route: return cstr(frappe.db.get_value('Item Group', self.item_group, - 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) + 'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5)) def validate_website_image(self): if frappe.flags.in_import: @@ -258,7 +252,6 @@ class Item(WebsiteGenerator): "attached_to_name": self.name }) except frappe.DoesNotExistError: - pass # cleanup frappe.local.message_log.pop() @@ -362,47 +355,49 @@ class Item(WebsiteGenerator): context.update(get_slideshow(self)) def set_attribute_context(self, context): - if self.has_variants: - attribute_values_available = {} - context.attribute_values = {} - context.selected_attributes = {} + if not self.has_variants: + return - # load attributes - for v in context.variants: - v.attributes = frappe.get_all("Item Variant Attribute", - fields=["attribute", "attribute_value"], - filters={"parent": v.name}) - # make a map for easier access in templates - v.attribute_map = frappe._dict({}) - for attr in v.attributes: - v.attribute_map[attr.attribute] = attr.attribute_value + attribute_values_available = {} + context.attribute_values = {} + context.selected_attributes = {} - for attr in v.attributes: - values = attribute_values_available.setdefault(attr.attribute, []) - if attr.attribute_value not in values: - values.append(attr.attribute_value) + # load attributes + for v in context.variants: + v.attributes = frappe.get_all("Item Variant Attribute", + fields=["attribute", "attribute_value"], + filters={"parent": v.name}) + # make a map for easier access in templates + v.attribute_map = frappe._dict({}) + for attr in v.attributes: + v.attribute_map[attr.attribute] = attr.attribute_value - if v.name == context.variant.name: - context.selected_attributes[attr.attribute] = attr.attribute_value + for attr in v.attributes: + values = attribute_values_available.setdefault(attr.attribute, []) + if attr.attribute_value not in values: + values.append(attr.attribute_value) - # filter attributes, order based on attribute table - for attr in self.attributes: - values = context.attribute_values.setdefault(attr.attribute, []) + if v.name == context.variant.name: + context.selected_attributes[attr.attribute] = attr.attribute_value - if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): - for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): - values.append(val) + # filter attributes, order based on attribute table + for attr in self.attributes: + values = context.attribute_values.setdefault(attr.attribute, []) - else: - # get list of values defined (for sequence) - for attr_value in frappe.db.get_all("Item Attribute Value", - fields=["attribute_value"], - filters={"parent": attr.attribute}, order_by="idx asc"): + if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): + for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): + values.append(val) - if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): - values.append(attr_value.attribute_value) + else: + # get list of values defined (for sequence) + for attr_value in frappe.db.get_all("Item Attribute Value", + fields=["attribute_value"], + filters={"parent": attr.attribute}, order_by="idx asc"): - context.variant_info = json.dumps(context.variants) + if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): + values.append(attr_value.attribute_value) + + context.variant_info = json.dumps(context.variants) def set_disabled_attributes(self, context): """Disable selection options of attribute combinations that do not result in a variant""" @@ -521,7 +516,7 @@ class Item(WebsiteGenerator): def validate_item_type(self): if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset: - msgprint(_("'Has Serial No' can not be 'Yes' for non-stock item"), raise_exception=1) + frappe.throw(_("'Has Serial No' can not be 'Yes' for non-stock item")) if self.has_serial_no == 0 and self.serial_no_series: self.serial_no_series = None @@ -542,10 +537,7 @@ class Item(WebsiteGenerator): def fill_customer_code(self): """ Append all the customer codes and insert into "customer_code" field of item table """ - cust_code = [] - for d in self.get('customer_items'): - cust_code.append(d.ref_code) - self.customer_code = ','.join(cust_code) + self.customer_code = ','.join(d.ref_code for d in self.get("customer_items", [])) def check_item_tax(self): """Check whether Tax Rate is not entered twice for same Tax Type""" @@ -742,23 +734,25 @@ class Item(WebsiteGenerator): def update_template_item(self): """Set Show in Website for Template Item if True for its Variant""" - if self.variant_of: - if self.show_in_website: - self.show_variant_in_website = 1 - self.show_in_website = 0 + if not self.variant_of: + return - if self.show_variant_in_website: - # show template - template_item = frappe.get_doc("Item", self.variant_of) + if self.show_in_website: + self.show_variant_in_website = 1 + self.show_in_website = 0 - if not template_item.show_in_website: - template_item.show_in_website = 1 - template_item.flags.dont_update_variants = True - template_item.flags.ignore_permissions = True - template_item.save() + if self.show_variant_in_website: + # show template + template_item = frappe.get_doc("Item", self.variant_of) + + if not template_item.show_in_website: + template_item.show_in_website = 1 + template_item.flags.dont_update_variants = True + template_item.flags.ignore_permissions = True + template_item.save() def validate_item_defaults(self): - companies = list(set([row.company for row in self.item_defaults])) + companies = {row.company for row in self.item_defaults} if len(companies) != len(self.item_defaults): frappe.throw(_("Cannot set multiple Item Defaults for a company.")) @@ -813,7 +807,7 @@ class Item(WebsiteGenerator): frappe.throw(_("Item has variants.")) def validate_attributes_in_variants(self): - if not self.has_variants or self.get("__islocal"): + if not self.has_variants or self.is_new(): return old_doc = self.get_doc_before_save() @@ -901,7 +895,7 @@ class Item(WebsiteGenerator): frappe.throw(_("Variant Based On cannot be changed")) def validate_uom(self): - if not self.get("__islocal"): + if not self.is_new(): check_stock_uom_with_bin(self.name, self.stock_uom) if self.has_variants: for d in frappe.db.get_all("Item", filters={"variant_of": self.name}): @@ -959,20 +953,20 @@ class Item(WebsiteGenerator): d.variant_of = self.variant_of def cant_change(self): - if not self.get("__islocal"): - fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no") + if self.is_new(): + return - values = frappe.db.get_value("Item", self.name, fields, as_dict=True) - if not values.get('valuation_method') and self.get('valuation_method'): - values['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO" + fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no") - if values: - for field in fields: - if cstr(self.get(field)) != cstr(values.get(field)): - if not self.check_if_linked_document_exists(field): - break # no linked document, allowed - else: - frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field)))) + values = frappe.db.get_value("Item", self.name, fields, as_dict=True) + if not values.get('valuation_method') and self.get('valuation_method'): + values['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO" + + if values: + for field in fields: + if cstr(self.get(field)) != cstr(values.get(field)): + if self.check_if_linked_document_exists(field): + frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field)))) def check_if_linked_document_exists(self, field): linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Receipt Item", @@ -1054,56 +1048,42 @@ def make_item_price(item, price_list_name, item_price): }).insert() def get_timeline_data(doctype, name): - '''returns timeline data based on stock ledger entry''' - out = {} - items = dict(frappe.db.sql('''select posting_date, count(*) - from `tabStock Ledger Entry` where item_code=%s - and posting_date > date_sub(curdate(), interval 1 year) - group by posting_date''', name)) + """get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page.""" - for date, count in iteritems(items): - timestamp = get_timestamp(date) - out.update({timestamp: count}) + items = frappe.db.sql("""select unix_timestamp(posting_date), count(*) + from `tabStock Ledger Entry` + where item_code=%s and posting_date > date_sub(curdate(), interval 1 year) + group by posting_date""", name) - return out + return dict(items) -def validate_end_of_life(item_code, end_of_life=None, disabled=None, verbose=1): + +def validate_end_of_life(item_code, end_of_life=None, disabled=None): if (not end_of_life) or (disabled is None): end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date(): - msg = _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) - _msgprint(msg, verbose) + frappe.throw(_("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life))) if disabled: - _msgprint(_("Item {0} is disabled").format(item_code), verbose) + frappe.throw(_("Item {0} is disabled").format(item_code)) -def validate_is_stock_item(item_code, is_stock_item=None, verbose=1): +def validate_is_stock_item(item_code, is_stock_item=None): if not is_stock_item: is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") if is_stock_item != 1: - msg = _("Item {0} is not a stock Item").format(item_code) - - _msgprint(msg, verbose) + frappe.throw(_("Item {0} is not a stock Item").format(item_code)) -def validate_cancelled_item(item_code, docstatus=None, verbose=1): +def validate_cancelled_item(item_code, docstatus=None): if docstatus is None: docstatus = frappe.db.get_value("Item", item_code, "docstatus") if docstatus == 2: - msg = _("Item {0} is cancelled").format(item_code) - _msgprint(msg, verbose) - -def _msgprint(msg, verbose): - if verbose: - msgprint(msg, raise_exception=True) - else: - raise frappe.ValidationError(msg) - + frappe.throw(_("Item {0} is cancelled").format(item_code)) def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): """returns last purchase details in stock uom""" @@ -1203,27 +1183,25 @@ def check_stock_uom_with_bin(item, stock_uom): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): return - matched = True ref_uom = frappe.db.get_value("Stock Ledger Entry", {"item_code": item}, "stock_uom") if ref_uom: if cstr(ref_uom) != cstr(stock_uom): - matched = False - else: - bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1) - for bin in bin_list: - if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0 - or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom): - matched = False - break + frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) - if matched and bin_list: - frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) + bin_list = frappe.db.sql(""" + select * from tabBin where item_code = %s + and (reserved_qty > 0 or ordered_qty > 0 or indented_qty > 0 or planned_qty > 0) + and stock_uom != %s + """, (item, stock_uom), as_dict=1) + + if bin_list: + frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.").format(item)) + + # No SLE or documents against item. Bin UOM can be changed safely. + frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) - if not matched: - frappe.throw( - _("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) def get_item_defaults(item_code, company): item = frappe.get_cached_doc('Item', item_code) @@ -1264,45 +1242,59 @@ def get_item_details(item_code, company=None): @frappe.whitelist() def get_uom_conv_factor(uom, stock_uom): - uoms = [uom, stock_uom] - value = "" - uom_details = frappe.db.sql("""select to_uom, from_uom, value from `tabUOM Conversion Factor`\ - where to_uom in ({0}) - """.format(', '.join([frappe.db.escape(i, percent=False) for i in uoms])), as_dict=True) + """ Get UOM conversion factor from uom to stock_uom + e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0 + """ + if uom == stock_uom: + return 1.0 - for d in uom_details: - if d.from_uom == stock_uom and d.to_uom == uom: - value = 1/flt(d.value) - elif d.from_uom == uom and d.to_uom == stock_uom: - value = d.value + from_uom, to_uom = uom, stock_uom # renaming for readability - if not value: - uom_stock = frappe.db.get_value("UOM Conversion Factor", {"to_uom": stock_uom}, ["from_uom", "value"], as_dict=1) - uom_row = frappe.db.get_value("UOM Conversion Factor", {"to_uom": uom}, ["from_uom", "value"], as_dict=1) + exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1) + if exact_match: + return exact_match.value - if uom_stock and uom_row: - if uom_stock.from_uom == uom_row.from_uom: - value = flt(uom_stock.value) * 1/flt(uom_row.value) + inverse_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1) + if inverse_match: + return 1 / inverse_match.value + + # This attempts to try and get conversion from intermediate UOM. + # case: + # g -> mg = 1000 + # g -> kg = 0.001 + # therefore kg -> mg = 1000 / 0.001 = 1,000,000 + intermediate_match = frappe.db.sql(""" + select (first.value / second.value) as value + from `tabUOM Conversion Factor` first + join `tabUOM Conversion Factor` second + on first.from_uom = second.from_uom + where + first.to_uom = %(to_uom)s + and second.to_uom = %(from_uom)s + limit 1 + """, {"to_uom": to_uom, "from_uom": from_uom}, as_dict=1) + + if intermediate_match: + return intermediate_match[0].value - return value @frappe.whitelist() -def get_item_attribute(parent, attribute_value=''): +def get_item_attribute(parent, attribute_value=""): + """Used for providing auto-completions in child table.""" if not frappe.has_permission("Item"): - frappe.msgprint(_("No Permission"), raise_exception=1) + frappe.throw(_("No Permission")) return frappe.get_all("Item Attribute Value", fields = ["attribute_value"], - filters = {'parent': parent, 'attribute_value': ("like", "%%%s%%" % attribute_value)}) + filters = {'parent': parent, 'attribute_value': ("like", f"%{attribute_value}%")}) def update_variants(variants, template, publish_progress=True): - count=0 - for d in variants: + total = len(variants) + for count, d in enumerate(variants, start=1): variant = frappe.get_doc("Item", d) copy_attributes_to_variant(template, variant) variant.save() - count+=1 if publish_progress: - frappe.publish_progress(count*100/len(variants), title = _("Updating Variants...")) + frappe.publish_progress(count / total * 100, title=_("Updating Variants...")) def on_doctype_update(): # since route is a Text column, it needs a length for indexing diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index e0b89d8e45..c7467a5a0f 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -10,14 +10,15 @@ from frappe.test_runner import make_test_objects from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsError, InvalidItemAttributeValueError, get_variant) from erpnext.stock.doctype.item.item import StockExistsForTemplate, InvalidBarcode -from erpnext.stock.doctype.item.item import get_uom_conv_factor +from erpnext.stock.doctype.item.item import (get_uom_conv_factor, get_item_attribute, + validate_is_stock_item, get_timeline_data) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.get_item_details import get_item_details +from erpnext.tests.utils import change_settings -from six import iteritems test_ignore = ["BOM"] -test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand"] +test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] def make_item(item_code, properties=None): if frappe.db.exists("Item", item_code): @@ -98,7 +99,7 @@ class TestItem(unittest.TestCase): "ignore_pricing_rule": 1 }) - for key, value in iteritems(to_check): + for key, value in to_check.items(): self.assertEqual(value, details.get(key)) def test_item_tax_template(self): @@ -194,7 +195,7 @@ class TestItem(unittest.TestCase): "plc_conversion_rate": 1, "customer": "_Test Customer", }) - for key, value in iteritems(sales_item_check): + for key, value in sales_item_check.items(): self.assertEqual(value, sales_item_details.get(key)) purchase_item_check = { @@ -215,7 +216,7 @@ class TestItem(unittest.TestCase): "plc_conversion_rate": 1, "supplier": "_Test Supplier", }) - for key, value in iteritems(purchase_item_check): + for key, value in purchase_item_check.items(): self.assertEqual(value, purchase_item_details.get(key)) def test_item_attribute_change_after_variant(self): @@ -375,6 +376,14 @@ class TestItem(unittest.TestCase): self.assertEqual(item_doc.uoms[1].uom, "Kg") self.assertEqual(item_doc.uoms[1].conversion_factor, 1000) + def test_uom_conv_intermediate(self): + factor = get_uom_conv_factor("Pound", "Gram") + self.assertAlmostEqual(factor, 453.592, 3) + + def test_uom_conv_base_case(self): + factor = get_uom_conv_factor("m", "m") + self.assertEqual(factor, 1.0) + def test_item_variant_by_manufacturer(self): fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}] set_item_variant_settings(fields) @@ -464,7 +473,7 @@ class TestItem(unittest.TestCase): self.assertEqual(len(matching_barcodes), 1) details = matching_barcodes[0] - for key, value in iteritems(barcode_properties): + for key, value in barcode_properties.items(): self.assertEqual(value, details.get(key)) # Add barcode again - should cause DuplicateEntryError @@ -480,6 +489,89 @@ class TestItem(unittest.TestCase): new_barcode.barcode_type = 'EAN' self.assertRaises(InvalidBarcode, item_doc.save) + def test_heatmap_data(self): + import time + data = get_timeline_data("Item", "_Test Item") + self.assertTrue(isinstance(data, dict)) + + now = time.time() + one_year_ago = now - 366 * 24 * 60 * 60 + + for timestamp, count in data.items(): + self.assertIsInstance(timestamp, int) + self.assertTrue(one_year_ago <= timestamp <= now) + self.assertIsInstance(count, int) + self.assertTrue(count >= 0) + + def test_index_creation(self): + "check if index is getting created in db" + from erpnext.stock.doctype.item.item import on_doctype_update + on_doctype_update() + + indices = frappe.db.sql("show index from tabItem", as_dict=1) + expected_columns = {"item_code", "item_name", "item_group", "route"} + for index in indices: + expected_columns.discard(index.get("Column_name")) + + if expected_columns: + self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}") + + def test_attribute_completions(self): + expected_attrs = {"Small", "Extra Small", "Extra Large", "Large", "2XL", "Medium"} + + attrs = get_item_attribute("Test Size") + received_attrs = {attr.attribute_value for attr in attrs} + self.assertEqual(received_attrs, expected_attrs) + + attrs = get_item_attribute("Test Size", attribute_value="extra") + received_attrs = {attr.attribute_value for attr in attrs} + self.assertEqual(received_attrs, {"Extra Small", "Extra Large"}) + + def test_check_stock_uom_with_bin(self): + # this item has opening stock and stock_uom set in test_records. + item = frappe.get_doc("Item", "_Test Item") + item.stock_uom = "Gram" + self.assertRaises(frappe.ValidationError, item.save) + + def test_check_stock_uom_with_bin_no_sle(self): + from erpnext.stock.stock_balance import update_bin_qty + item = create_item("_Item with bin qty") + item.stock_uom = "Gram" + item.save() + + update_bin_qty(item.item_code, "_Test Warehouse - _TC", { + "reserved_qty": 10 + }) + + item.stock_uom = "Kilometer" + self.assertRaises(frappe.ValidationError, item.save) + + update_bin_qty(item.item_code, "_Test Warehouse - _TC", { + "reserved_qty": 0 + }) + + item.load_from_db() + item.stock_uom = "Kilometer" + try: + item.save() + except frappe.ValidationError as e: + self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}") + + def test_validate_stock_item(self): + self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item") + + try: + validate_is_stock_item("_Test Item") + except frappe.ValidationError as e: + self.fail(f"stock item considered non-stock item: {e}") + + @change_settings("Stock Settings", {"item_naming_by": "Naming Series"}) + def test_autoname_series(self): + item = frappe.new_doc("Item") + item.item_group = "All Item Groups" + item.save() # if item code saved without item_code then series worked + + def set_item_variant_settings(fields): doc = frappe.get_doc('Item Variant Settings') doc.set('fields', fields) @@ -494,23 +586,24 @@ def make_item_variant(): test_records = frappe.get_test_records('Item') -def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, is_customer_provided_item=None, - customer=None, is_purchase_item=None, opening_stock=None, company=None): +def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC", + is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, + company="_Test Company"): if not frappe.db.exists("Item", item_code): item = frappe.new_doc("Item") item.item_code = item_code item.item_name = item_code item.description = item_code item.item_group = "All Item Groups" - item.is_stock_item = is_stock_item or 1 - item.opening_stock = opening_stock or 0 - item.valuation_rate = valuation_rate or 0.0 + item.is_stock_item = is_stock_item + item.opening_stock = opening_stock + item.valuation_rate = valuation_rate item.is_purchase_item = is_purchase_item item.is_customer_provided_item = is_customer_provided_item item.customer = customer or '' item.append("item_defaults", { - "default_warehouse": warehouse or '_Test Warehouse - _TC', - "company": company or "_Test Company" + "default_warehouse": warehouse, + "company": company }) item.save() else: diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index f1d7f8c8c9..bb396e806f 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -13,6 +13,7 @@ "section_break_6", "warehouse", "target_warehouse", + "conversion_factor", "column_break_9", "qty", "uom", @@ -209,13 +210,18 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-24 09:25:13.050151", + "modified": "2021-05-26 07:08:05.111385", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 5341f29853..4ab71bdf62 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -53,6 +53,7 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip pi.parent_detail_docname = main_item_row.name pi.uom = item.stock_uom pi.qty = flt(qty) + pi.conversion_factor = main_item_row.conversion_factor if description and not pi.description: pi.description = description if not pi.warehouse and not doc.amended_from: diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 32d349f303..ad350d344f 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -762,6 +762,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "base_rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment (Company Currency)", @@ -805,6 +806,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment", @@ -1147,7 +1149,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-12-26 20:49:39.106049", + "modified": "2021-04-19 01:01:00.754119", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index f1292d8cbd..83ba324495 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -497,7 +497,7 @@ class PurchaseReceipt(BuyingController): def update_billing_status(self, update_modified=True): updated_pr = [self.name] for d in self.get("items"): - if d.purchase_invoice and d.purchase_invoice_item: + if d.get("purchase_invoice") and d.get("purchase_invoice_item"): d.db_set('billed_amt', d.amount, update_modified=update_modified) elif d.purchase_order_item: updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified) @@ -748,4 +748,3 @@ def get_item_account_wise_additional_cost(purchase_document): account.base_amount * item.get(based_on_field) / total_item_cost return item_account_wise_cost - diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index e5ef978ca3..5095a80214 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -297,6 +297,8 @@ class TestPurchaseReceipt(unittest.TestCase): item_code = "Test Extra Item 1", qty=10, basic_rate=100) se2 = make_stock_entry(target="_Test Warehouse - _TC", item_code = "_Test FG Item", qty=1, basic_rate=100) + se3 = make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Test Extra Item 2", qty=1, basic_rate=100) rm_items = [ { "item_code": item_code, @@ -331,6 +333,7 @@ class TestPurchaseReceipt(unittest.TestCase): se.cancel() se1.cancel() se2.cancel() + se3.cancel() po.reload() po.cancel() diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 56b046a92e..7f3d701034 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -1,29 +1,45 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + +import frappe from frappe.utils import nowdate -from erpnext.stock.doctype.item.test_item import create_item + +from erpnext.controllers.stock_controller import ( + QualityInspectionNotSubmittedError, + QualityInspectionRejectedError, + QualityInspectionRequiredError, + make_quality_inspections, +) from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError, QualityInspectionNotSubmittedError # test_records = frappe.get_test_records('Quality Inspection') + class TestQualityInspection(unittest.TestCase): def setUp(self): create_item("_Test Item with QA") - frappe.db.set_value("Item", "_Test Item with QA", "inspection_required_before_delivery", 1) + frappe.db.set_value( + "Item", "_Test Item with QA", "inspection_required_before_delivery", 1 + ) def test_qa_for_delivery(self): - make_stock_entry(item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100) + make_stock_entry( + item_code="_Test Item with QA", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=100 + ) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) self.assertRaises(QualityInspectionRequiredError, dn.submit) - qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected") + qa = create_quality_inspection( + reference_type="Delivery Note", reference_name=dn.name, status="Rejected" + ) dn.reload() self.assertRaises(QualityInspectionRejectedError, dn.submit) @@ -38,7 +54,9 @@ class TestQualityInspection(unittest.TestCase): def test_qa_not_submit(self): dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) - qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True) + qa = create_quality_inspection( + reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True + ) dn.items[0].quality_inspection = qa.name self.assertRaises(QualityInspectionNotSubmittedError, dn.submit) @@ -48,21 +66,28 @@ class TestQualityInspection(unittest.TestCase): def test_value_based_qi_readings(self): # Test QI based on acceptance values (Non formula) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) - readings = [{ - "specification": "Iron Content", # numeric reading - "min_value": 0.1, - "max_value": 0.9, - "reading_1": "0.4" - }, - { - "specification": "Particle Inspection Needed", # non-numeric reading - "numeric": 0, - "value": "Yes", - "reading_value": "Yes" - }] + readings = [ + { + "specification": "Iron Content", # numeric reading + "min_value": 0.1, + "max_value": 0.9, + "reading_1": "0.4" + }, + { + "specification": "Particle Inspection Needed", # non-numeric reading + "numeric": 0, + "value": "Yes", + "reading_value": "Yes" + } + ] + + qa = create_quality_inspection( + reference_type="Delivery Note", + reference_name=dn.name, + readings=readings, + do_not_save=True + ) - qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, - readings=readings, do_not_save=True) qa.save() # status must be auto set as per formula @@ -74,36 +99,43 @@ class TestQualityInspection(unittest.TestCase): def test_formula_based_qi_readings(self): dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) - readings = [{ - "specification": "Iron Content", # numeric reading - "formula_based_criteria": 1, - "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", - "reading_1": "0.4" - }, - { - "specification": "Calcium Content", # numeric reading - "formula_based_criteria": 1, - "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", - "reading_1": "0.7" - }, - { - "specification": "Mg Content", # numeric reading - "formula_based_criteria": 1, - "acceptance_formula": "mean < 0.9", - "reading_1": "0.5", - "reading_2": "0.7", - "reading_3": "random text" # check if random string input causes issues - }, - { - "specification": "Calcium Content", # non-numeric reading - "formula_based_criteria": 1, - "numeric": 0, - "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')", - "reading_value": "Grade B" - }] + readings = [ + { + "specification": "Iron Content", # numeric reading + "formula_based_criteria": 1, + "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", + "reading_1": "0.4" + }, + { + "specification": "Calcium Content", # numeric reading + "formula_based_criteria": 1, + "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", + "reading_1": "0.7" + }, + { + "specification": "Mg Content", # numeric reading + "formula_based_criteria": 1, + "acceptance_formula": "mean < 0.9", + "reading_1": "0.5", + "reading_2": "0.7", + "reading_3": "random text" # check if random string input causes issues + }, + { + "specification": "Calcium Content", # non-numeric reading + "formula_based_criteria": 1, + "numeric": 0, + "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')", + "reading_value": "Grade B" + } + ] + + qa = create_quality_inspection( + reference_type="Delivery Note", + reference_name=dn.name, + readings=readings, + do_not_save=True + ) - qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, - readings=readings, do_not_save=True) qa.save() # status must be auto set as per formula @@ -115,6 +147,19 @@ class TestQualityInspection(unittest.TestCase): qa.delete() dn.delete() + def test_make_quality_inspections_from_linked_document(self): + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + for item in dn.items: + item.sample_size = item.qty + quality_inspections = make_quality_inspections(dn.doctype, dn.name, dn.items) + self.assertEqual(len(dn.items), len(quality_inspections)) + + # cleanup + for qi in quality_inspections: + frappe.delete_doc("Quality Inspection", qi) + dn.delete() + + def create_quality_inspection(**args): args = frappe._dict(args) qa = frappe.new_doc("Quality Inspection") @@ -134,7 +179,7 @@ def create_quality_inspection(**args): readings = args.readings if args.status == "Rejected": - readings["reading_1"] = "12" # status is auto set in child on save + readings["reading_1"] = "12" # status is auto set in child on save if isinstance(readings, list): for entry in readings: @@ -150,10 +195,11 @@ def create_quality_inspection(**args): return qa + def create_quality_inspection_parameter(parameter): if not frappe.db.exists("Quality Inspection Parameter", parameter): frappe.get_doc({ "doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter - }).insert() \ No newline at end of file + }).insert() 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 5b626ea345..55f2ebb224 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -37,6 +37,9 @@ class RepostItemValuation(Document): self.db_set('status', status) def on_submit(self): + if not frappe.flags.in_test: + return + frappe.enqueue(repost, timeout=1800, queue='long', job_name='repost_sle', now=frappe.flags.in_test, doc=self) @@ -115,12 +118,6 @@ def notify_error_to_stock_managers(doc, traceback): frappe.sendmail(recipients=recipients, subject=subject, message=message) def repost_entries(): - job_log = frappe.get_all('Scheduled Job Log', fields = ['status', 'creation'], - filters = {'scheduled_job_type': 'repost_item_valuation.repost_entries'}, order_by='creation desc', limit=1) - - if job_log and job_log[0]['status'] == 'Start' and time_diff_in_hours(now(), job_log[0]['creation']) < 2: - return - riv_entries = get_repost_item_valuation_entries() for row in riv_entries: @@ -135,9 +132,7 @@ def repost_entries(): check_if_stock_and_account_balance_synced(today(), d.name) def get_repost_item_valuation_entries(): - date = add_to_date(now(), hours=-3) - return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` WHERE status != 'Completed' and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - """, date, as_dict=1) + """, now(), as_dict=1) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index de23e769f8..93a6fc0e0a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -115,6 +115,14 @@ frappe.ui.form.on('Stock Entry', { return; } + if (!frm.is_new() && frm.doc.docstatus === 0) { + frm.add_custom_button(__("Quality Inspection(s)"), () => { + let transaction_controller = new erpnext.TransactionController({ frm: frm }); + transaction_controller.make_quality_inspection(); + }, __("Create")); + frm.page.set_inner_btn_group_as_primary(__('Create')); + } + let quality_inspection_field = frm.get_docfield("items", "quality_inspection"); quality_inspection_field.get_route_options_for_new_doc = function(row) { if (frm.is_new()) return; @@ -155,7 +163,7 @@ frappe.ui.form.on('Stock Entry', { refresh: function(frm) { if(!frm.doc.docstatus) { frm.trigger('validate_purpose_consumption'); - frm.add_custom_button(__('Create Material Request'), function() { + frm.add_custom_button(__('Material Request'), function() { frappe.model.with_doctype('Material Request', function() { var mr = frappe.model.get_new_doc('Material Request'); var items = frm.get_field('items').grid.get_selected_children(); @@ -178,7 +186,7 @@ frappe.ui.form.on('Stock Entry', { }); frappe.set_route('Form', 'Material Request', mr.name); }); - }); + }, __("Create")); } if(frm.doc.items) { 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 3296f5ba4a..ba31ad7b06 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 @@ -15,10 +15,12 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import create_landed_cost_voucher from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDatedStockTransaction +from frappe.core.page.permission_manager.permission_manager import reset class TestStockLedgerEntry(unittest.TestCase): def setUp(self): items = create_items() + reset('Stock Entry') # delete SLE and BINs for all items frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items) @@ -314,10 +316,11 @@ class TestStockLedgerEntry(unittest.TestCase): # Set User with Stock User role but not Stock Manager try: user = frappe.get_doc("User", "test@example.com") - frappe.set_user(user.name) user.add_roles("Stock User") user.remove_roles("Stock Manager") + frappe.set_user(user.name) + stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, posting_date=add_days(today(), -1), do_not_submit=True) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 7e216d6181..306df99b3c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -96,7 +96,7 @@ class StockReconciliation(StockController): def validate_data(self): def _get_msg(row_num, msg): - return _("Row # {0}: ").format(row_num+1) + msg + return _("Row # {0}:").format(row_num+1) + " " + msg self.validation_messages = [] item_warehouse_combinations = [] @@ -167,8 +167,8 @@ class StockReconciliation(StockController): item = frappe.get_doc("Item", item_code) # end of life and stock item - validate_end_of_life(item_code, item.end_of_life, item.disabled, verbose=0) - validate_is_stock_item(item_code, item.is_stock_item, verbose=0) + validate_end_of_life(item_code, item.end_of_life, item.disabled) + validate_is_stock_item(item_code, item.is_stock_item) # item should not be serialized if item.has_serial_no and not row.serial_no and not item.serial_no_series: @@ -179,10 +179,10 @@ class StockReconciliation(StockController): raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code)) # docstatus should be < 2 - validate_cancelled_item(item_code, item.docstatus, verbose=0) + validate_cancelled_item(item_code, item.docstatus) except Exception as e: - self.validation_messages.append(_("Row # ") + ("%d: " % (row.idx)) + cstr(e)) + self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e)) def update_stock_ledger(self): """ find difference between current and expected entries @@ -477,19 +477,19 @@ class StockReconciliation(StockController): def get_items(warehouse, posting_date, posting_time, company): lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) items = frappe.db.sql(""" - select i.name, i.item_name, bin.warehouse + select i.name, i.item_name, bin.warehouse, i.has_serial_no from tabBin bin, tabItem i where i.name=bin.item_code and i.disabled=0 and i.is_stock_item = 1 - and i.has_variants = 0 and i.has_serial_no = 0 and i.has_batch_no = 0 + and i.has_variants = 0 and i.has_batch_no = 0 and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse) """, (lft, rgt)) items += frappe.db.sql(""" - select i.name, i.item_name, id.default_warehouse + select i.name, i.item_name, id.default_warehouse, i.has_serial_no from tabItem i, `tabItem Default` id where i.name = id.parent and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse) - and i.is_stock_item = 1 and i.has_serial_no = 0 and i.has_batch_no = 0 + and i.is_stock_item = 1 and i.has_batch_no = 0 and i.has_variants = 0 and i.disabled = 0 and id.company=%s group by i.name """, (lft, rgt, company)) @@ -497,7 +497,7 @@ def get_items(warehouse, posting_date, posting_time, company): res = [] for d in set(items): stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time, - with_valuation_rate=True) + with_valuation_rate=True , with_serial_no=cint(d[3])) if frappe.db.get_value("Item", d[0], "disabled") == 0: res.append({ @@ -507,7 +507,9 @@ def get_items(warehouse, posting_date, posting_time, company): "item_name": d[1], "valuation_rate": stock_bal[1], "current_qty": stock_bal[0], - "current_valuation_rate": stock_bal[1] + "current_valuation_rate": stock_bal[1], + "current_serial_no": stock_bal[2] if cint(d[3]) else '', + "serial_no": stock_bal[2] if cint(d[3]) else '' }) return res diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 8ba1f1ca5c..8917bfeae4 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -194,9 +194,6 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin serial_nos = frappe.db.sql("""select count(name) from `tabSerial No` where item_code=%s and warehouse=%s and docstatus < 2""", (d[0], d[1])) - if serial_nos and flt(serial_nos[0][0]) != flt(d[2]): - print(d[0], d[1], d[2], serial_nos[0][0]) - sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry` where item_code = %s and warehouse = %s and is_cancelled = 0 order by posting_date desc limit 1""", (d[0], d[1])) @@ -230,7 +227,7 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin }) update_bin(args) - + create_repost_item_valuation_entry({ "item_code": d[0], "warehouse": d[1], diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index a43381c5c6..bc29821ee2 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -119,7 +119,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nReplied\nHold\nResolved\nClosed", + "options": "Open\nReplied\nOn Hold\nResolved\nClosed", "search_index": 1 }, { @@ -410,7 +410,7 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-08-11 18:49:07.574769", + "modified": "2021-05-26 10:49:07.574769", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/report/issue_summary/issue_summary.js b/erpnext/support/report/issue_summary/issue_summary.js index eb0e06cd08..a5122d03ad 100644 --- a/erpnext/support/report/issue_summary/issue_summary.js +++ b/erpnext/support/report/issue_summary/issue_summary.js @@ -42,6 +42,7 @@ frappe.query_reports["Issue Summary"] = { "", {label: __('Open'), value: 'Open'}, {label: __('Replied'), value: 'Replied'}, + {label: __('On Hold'), value: 'On Hold'}, {label: __('Resolved'), value: 'Resolved'}, {label: __('Closed'), value: 'Closed'} ] diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index 7861e30d25..bba25b8bed 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -62,7 +62,7 @@ class IssueSummary(object): 'width': 200 }) - self.statuses = ['Open', 'Replied', 'Resolved', 'Closed'] + self.statuses = ['Open', 'Replied', 'On Hold', 'Resolved', 'Closed'] for status in self.statuses: self.columns.append({ 'label': _(status), @@ -265,6 +265,7 @@ class IssueSummary(object): labels = [] open_issues = [] replied_issues = [] + on_hold_issues = [] resolved_issues = [] closed_issues = [] @@ -277,6 +278,7 @@ class IssueSummary(object): labels.append(entry.get(entity_field)) open_issues.append(entry.get('open')) replied_issues.append(entry.get('replied')) + on_hold_issues.append(entry.get('on_hold')) resolved_issues.append(entry.get('resolved')) closed_issues.append(entry.get('closed')) @@ -292,6 +294,10 @@ class IssueSummary(object): 'name': 'Replied', 'values': replied_issues[:30] }, + { + 'name': 'On Hold', + 'values': on_hold_issues[:30] + }, { 'name': 'Resolved', 'values': resolved_issues[:30] @@ -313,12 +319,14 @@ class IssueSummary(object): open_issues = 0 replied = 0 + on_hold = 0 resolved = 0 closed = 0 for entry in self.data: open_issues += entry.get('open') replied += entry.get('replied') + on_hold += entry.get('on_hold') resolved += entry.get('resolved') closed += entry.get('closed') @@ -335,6 +343,12 @@ class IssueSummary(object): 'label': _('Replied'), 'datatype': 'Int', }, + { + 'value': on_hold, + 'indicator': 'Grey', + 'label': _('On Hold'), + 'datatype': 'Int', + }, { 'value': resolved, 'indicator': 'Green', diff --git a/erpnext/tests/__init__.py b/erpnext/tests/__init__.py index e69de29bb2..a504340d40 100644 --- a/erpnext/tests/__init__.py +++ b/erpnext/tests/__init__.py @@ -0,0 +1 @@ +global_test_dependencies = ['User', 'Company', 'Item'] diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 16ecd5180b..11eb6afc1f 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -1,7 +1,8 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +import copy +from contextlib import contextmanager import frappe @@ -41,3 +42,38 @@ def create_test_contact_and_address(): contact.add_email("test_contact_customer@example.com", is_primary=True) contact.add_phone("+91 0000000000", is_primary_phone=True) contact.insert() + + +@contextmanager +def change_settings(doctype, settings_dict): + """ A context manager to ensure that settings are changed before running + function and restored after running it regardless of exceptions occured. + This is useful in tests where you want to make changes in a function but + don't retain those changes. + import and use as decorator to cover full function or using `with` statement. + + example: + @change_settings("Stock Settings", {"item_naming_by": "Naming Series"}) + def test_case(self): + ... + """ + + try: + settings = frappe.get_doc(doctype) + # remember setting + previous_settings = copy.deepcopy(settings_dict) + for key in previous_settings: + previous_settings[key] = getattr(settings, key) + + # change setting + for key, value in settings_dict.items(): + setattr(settings, key, value) + settings.save() + yield # yield control to calling function + + finally: + # restore settings + settings = frappe.get_doc(doctype) + for key, value in previous_settings.items(): + setattr(settings, key, value) + settings.save() diff --git a/erpnext/utilities/__init__.py b/erpnext/utilities/__init__.py index 618cc985ae..0a5aa3c49b 100644 --- a/erpnext/utilities/__init__.py +++ b/erpnext/utilities/__init__.py @@ -12,7 +12,6 @@ def update_doctypes(): for f in dt.fields: if f.fieldname == d.fieldname and f.fieldtype in ("Text", "Small Text"): - print(f.parent, f.fieldname) f.fieldtype = "Text Editor" dt.save() break diff --git a/erpnext/www/lms/content.html b/erpnext/www/lms/content.html index 15afb097b9..d22ef66d2a 100644 --- a/erpnext/www/lms/content.html +++ b/erpnext/www/lms/content.html @@ -64,6 +64,7 @@

{{ content.name }} ({{ position + 1 }}/{{length}})

+
{% endmacro %}