diff --git a/.flake8 b/.flake8 index 4ff88403244..4b852abd7c6 100644 --- a/.flake8 +++ b/.flake8 @@ -31,6 +31,7 @@ ignore = E124, # closing bracket, irritating while writing QB code E131, # continuation line unaligned for hanging indent E123, # closing bracket does not match indentation of opening bracket's line + E101, # ensured by use of black max-line-length = 200 exclude=.github/helper/semgrep_rules diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml deleted file mode 100644 index ab6a53b5d92..00000000000 --- a/.github/workflows/ui-tests.yml +++ /dev/null @@ -1,117 +0,0 @@ -name: UI - -on: - pull_request: - paths-ignore: - - '**.md' - workflow_dispatch: - -concurrency: - group: ui-develop-${{ github.event.number }} - cancel-in-progress: true - -jobs: - test: - runs-on: ubuntu-latest - timeout-minutes: 60 - - strategy: - fail-fast: false - - name: UI Tests (Cypress) - - 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.8 - - - uses: actions/setup-node@v2 - with: - node-version: 14 - check-latest: true - - - 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: Cache cypress binary - uses: actions/cache@v2 - with: - path: ~/.cache - key: ${{ runner.os }}-cypress- - restore-keys: | - ${{ runner.os }}-cypress- - ${{ runner.os }}- - - - name: Install - run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh - env: - DB: mariadb - TYPE: ui - - - name: Site Setup - run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests - - - name: cypress pre-requisites - run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 @testing-library/cypress@^8 --no-lockfile - - - - name: Build Assets - run: cd ~/frappe-bench/ && bench build - env: - CI: Yes - - - name: UI Tests - run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless - env: - CYPRESS_RECORD_KEY: 60a8e3bf-08f5-45b1-9269-2b207d7d30cd - - - name: Show bench console if tests failed - if: ${{ failure() }} - run: cat ~/frappe-bench/bench_run_logs.txt diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js index 9074defa577..7d85d89c452 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.js +++ b/erpnext/accounts/doctype/payment_order/payment_order.js @@ -12,7 +12,6 @@ frappe.ui.form.on('Payment Order', { }); frm.set_df_property('references', 'cannot_add_rows', true); - frm.set_df_property('references', 'cannot_delete_rows', true); }, refresh: function(frm) { if (frm.doc.docstatus == 0) { diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 08cec6a858a..c45b069730e 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -375,12 +375,12 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa def update_args_for_pricing_rule(args): if not (args.item_group and args.brand): - try: - args.item_group, args.brand = frappe.get_cached_value( - "Item", args.item_code, ["item_group", "brand"] - ) - except frappe.DoesNotExistError: + item = frappe.get_cached_value("Item", args.item_code, ("item_group", "brand")) + if not item: return + + args.item_group, args.brand = item + if not args.item_group: frappe.throw(_("Item Group not mentioned in item master for item {0}").format(args.item_code)) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 81c60bb337d..f6961eb95fa 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -53,6 +53,22 @@ frappe.query_reports["Accounts Payable"] = { } } }, + { + "fieldname": "party_account", + "label": __("Payable Account"), + "fieldtype": "Link", + "options": "Account", + get_query: () => { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + 'company': company, + 'account_type': 'Payable', + 'is_group': 0 + } + }; + } + }, { "fieldname": "ageing_based_on", "label": __("Ageing Based On"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 570029851e8..748bcde4354 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -66,6 +66,22 @@ frappe.query_reports["Accounts Receivable"] = { } } }, + { + "fieldname": "party_account", + "label": __("Receivable Account"), + "fieldtype": "Link", + "options": "Account", + get_query: () => { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + 'company': company, + 'account_type': 'Receivable', + 'is_group': 0 + } + }; + } + }, { "fieldname": "ageing_based_on", "label": __("Ageing Based On"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 7bf9539b751..de9d63d849a 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -111,6 +111,7 @@ class ReceivablePayableReport(object): voucher_type=gle.voucher_type, voucher_no=gle.voucher_no, party=gle.party, + party_account=gle.account, posting_date=gle.posting_date, account_currency=gle.account_currency, remarks=gle.remarks if self.filters.get("show_remarks") else None, @@ -777,18 +778,22 @@ class ReceivablePayableReport(object): conditions.append("party=%s") values.append(self.filters.get(party_type_field)) - # get GL with "receivable" or "payable" account_type - account_type = "Receivable" if self.party_type == "Customer" else "Payable" - accounts = [ - d.name - for d in frappe.get_all( - "Account", filters={"account_type": account_type, "company": self.filters.company} - ) - ] + if self.filters.party_account: + conditions.append("account =%s") + values.append(self.filters.party_account) + else: + # get GL with "receivable" or "payable" account_type + account_type = "Receivable" if self.party_type == "Customer" else "Payable" + accounts = [ + d.name + for d in frappe.get_all( + "Account", filters={"account_type": account_type, "company": self.filters.company} + ) + ] - if accounts: - conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts))) - values += accounts + if accounts: + conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts))) + values += accounts def add_customer_filters(self, conditions, values): if self.filters.get("customer_group"): @@ -888,6 +893,13 @@ class ReceivablePayableReport(object): options=self.party_type, width=180, ) + self.add_column( + label="Receivable Account" if self.party_type == "Customer" else "Payable Account", + fieldname="party_account", + fieldtype="Link", + options="Account", + width=180, + ) if self.party_naming_by == "Naming Series": self.add_column( diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 7a6989f9e54..f38890e980c 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -50,12 +50,19 @@ class TestAccountsReceivable(unittest.TestCase): make_credit_note(name) report = execute(filters) - expected_data_after_credit_note = [100, 0, 0, 40, -40] + expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"] row = report[1][0] self.assertEqual( expected_data_after_credit_note, - [row.invoice_grand_total, row.invoiced, row.paid, row.credit_note, row.outstanding], + [ + row.invoice_grand_total, + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + row.party_account, + ], ) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8c0112bad0f..fefb2e59f01 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1016,7 +1016,7 @@ def get_bom_items_as_dict( query = query.format( table="BOM Scrap Item", where_conditions="", - select_columns=", bom_item.idx, item.description, is_process_loss", + select_columns=", item.description, is_process_loss", is_stock_item=is_stock_item, qty_field="stock_qty", ) @@ -1029,7 +1029,7 @@ def get_bom_items_as_dict( is_stock_item=is_stock_item, qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty", select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, - bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, + bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, bom_item.description, bom_item.base_rate as rate """, ) items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 37217048401..8934f9c4e09 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1114,6 +1114,36 @@ class TestWorkOrder(FrappeTestCase): except frappe.MandatoryError: self.fail("Batch generation causing failing in Work Order") + @change_settings( + "Manufacturing Settings", + {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, + ) + def test_manufacture_entry_mapped_idx_with_exploded_bom(self): + """Test if WO containing BOM with partial exploded items and scrap items, maps idx correctly.""" + test_stock_entry.make_stock_entry( + item_code="_Test Item", + target="_Test Warehouse - _TC", + basic_rate=5000.0, + qty=2, + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + basic_rate=1000.0, + qty=2, + ) + + wo_order = make_wo_order_test_record( + qty=1, + use_multi_level_bom=1, + skip_transfer=1, + ) + + ste_manu = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 1)) + + for index, row in enumerate(ste_manu.get("items"), start=1): + self.assertEqual(index, row.idx) + def update_job_card(job_card, jc_qty=None): employee = frappe.db.get_value("Employee", {"status": "Active"}, "name") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index c8c2f9a9320..2ee848c356a 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1327,7 +1327,7 @@ def get_serial_nos_for_job_card(row, wo_doc): used_serial_nos.extend(get_serial_nos(d.serial_no)) serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos))) - row.serial_no = "\n".join(serial_nos[0 : row.job_card_qty]) + row.serial_no = "\n".join(serial_nos[0 : cint(row.job_card_qty)]) def validate_operation_data(row): diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index 256a3b55301..18bd3b7733c 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -147,6 +147,8 @@ class AdditionalSalary(Document): @frappe.whitelist() def get_additional_salaries(employee, start_date, end_date, component_type): + from frappe.query_builder import Criterion + comp_type = "Earning" if component_type == "earnings" else "Deduction" additional_sal = frappe.qb.DocType("Additional Salary") @@ -170,8 +172,23 @@ def get_additional_salaries(employee, start_date, end_date, component_type): & (additional_sal.type == comp_type) ) .where( - additional_sal.payroll_date[start_date:end_date] - | ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date)) + Criterion.any( + [ + Criterion.all( + [ # is recurring and additional salary dates fall within the payroll period + additional_sal.is_recurring == 1, + additional_sal.from_date <= end_date, + additional_sal.to_date >= end_date, + ] + ), + Criterion.all( + [ # is not recurring and additional salary's payroll date falls within the payroll period + additional_sal.is_recurring == 0, + additional_sal.payroll_date[start_date:end_date], + ] + ), + ] + ) ) .run(as_dict=True) ) diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py index 7d5d9e02f34..bd739368a0a 100644 --- a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py @@ -4,7 +4,8 @@ import unittest import frappe -from frappe.utils import add_days, nowdate +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, add_months, nowdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee @@ -16,19 +17,10 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure -class TestAdditionalSalary(unittest.TestCase): +class TestAdditionalSalary(FrappeTestCase): def setUp(self): setup_test() - def tearDown(self): - for dt in [ - "Salary Slip", - "Additional Salary", - "Salary Structure Assignment", - "Salary Structure", - ]: - frappe.db.sql("delete from `tab%s`" % dt) - def test_recurring_additional_salary(self): amount = 0 salary_component = None @@ -46,19 +38,66 @@ class TestAdditionalSalary(unittest.TestCase): if earning.salary_component == "Recurring Salary Component": amount = earning.amount salary_component = earning.salary_component + break self.assertEqual(amount, add_sal.amount) self.assertEqual(salary_component, add_sal.salary_component) + def test_non_recurring_additional_salary(self): + amount = 0 + salary_component = None + date = nowdate() -def get_additional_salary(emp_id): + emp_id = make_employee("test_additional@salary.com") + frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(date, 1800)) + salary_structure = make_salary_structure( + "Test Salary Structure Additional Salary", "Monthly", employee=emp_id + ) + add_sal = get_additional_salary(emp_id, recurring=False, payroll_date=date) + + ss = make_employee_salary_slip( + "test_additional@salary.com", "Monthly", salary_structure=salary_structure.name + ) + + amount, salary_component = None, None + for earning in ss.earnings: + if earning.salary_component == "Recurring Salary Component": + amount = earning.amount + salary_component = earning.salary_component + break + + self.assertEqual(amount, add_sal.amount) + self.assertEqual(salary_component, add_sal.salary_component) + + # should not show up in next months + ss.posting_date = add_months(date, 1) + ss.start_date = ss.end_date = None + ss.earnings = [] + ss.deductions = [] + ss.save() + + amount, salary_component = None, None + for earning in ss.earnings: + if earning.salary_component == "Recurring Salary Component": + amount = earning.amount + salary_component = earning.salary_component + break + + self.assertIsNone(amount) + self.assertIsNone(salary_component) + + +def get_additional_salary(emp_id, recurring=True, payroll_date=None): create_salary_component("Recurring Salary Component") add_sal = frappe.new_doc("Additional Salary") add_sal.employee = emp_id add_sal.salary_component = "Recurring Salary Component" - add_sal.is_recurring = 1 + + add_sal.is_recurring = 1 if recurring else 0 add_sal.from_date = add_days(nowdate(), -50) add_sal.to_date = add_days(nowdate(), 180) + add_sal.payroll_date = payroll_date + add_sal.amount = 5000 add_sal.currency = erpnext.get_default_currency() add_sal.save() diff --git a/erpnext/stock/dashboard/item_dashboard.html b/erpnext/stock/dashboard/item_dashboard.html index 99698ba69a9..b7a786e50f4 100644 --- a/erpnext/stock/dashboard/item_dashboard.html +++ b/erpnext/stock/dashboard/item_dashboard.html @@ -1,4 +1,4 @@ -